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/README.md +565 -112
- package/dist/badge.d.ts +13 -0
- package/dist/{chunk-NRMMGQRU.js → chunk-OA3YX73Z.js} +35 -12
- package/dist/chunk-OA3YX73Z.js.map +1 -0
- package/dist/color.d.ts +6 -0
- package/dist/detect.d.ts +14 -0
- package/dist/favicon-env.global.js +1 -1
- package/dist/favicon-env.global.js.map +1 -1
- package/dist/filter.d.ts +7 -0
- package/dist/hash.d.ts +9 -0
- package/dist/index.d.ts +5 -54
- package/dist/index.js +21 -8
- package/dist/index.js.map +1 -1
- package/dist/ssr.d.ts +20 -1
- package/dist/ssr.js +1 -1
- package/dist/tint.d.ts +25 -0
- package/dist/{ssr-CI5GvAY9.d.ts → types.d.ts} +23 -32
- package/package.json +44 -33
- package/skills/core/SKILL.md +157 -46
- package/dist/chunk-NRMMGQRU.js.map +0 -1
package/dist/ssr.d.ts
CHANGED
|
@@ -1 +1,20 @@
|
|
|
1
|
-
|
|
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
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`
|
|
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.
|
|
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
|
-
"
|
|
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/
|
|
24
|
+
"url": "git+https://github.com/Amir-Abushanab/favicon-env.git"
|
|
24
25
|
},
|
|
25
|
-
"
|
|
26
|
-
|
|
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.
|
|
59
|
-
"oxfmt": "^0.
|
|
60
|
-
"oxlint": "^1.
|
|
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": "^
|
|
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",
|
package/skills/core/SKILL.md
CHANGED
|
@@ -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/
|
|
8
|
-
detect, environments, rules, or
|
|
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.
|
|
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 —
|
|
31
|
+
Runtime — gate the import with a compile-time public environment value:
|
|
31
32
|
|
|
32
33
|
```js
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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`,
|
|
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
|