favicon-env 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Amir Abushanab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,314 @@
1
+ # favicon-env
2
+
3
+ Tint your favicon per environment so you can tell instances apart at a glance — no more staring at three identical tabs wondering which one is production.
4
+
5
+ ![favicon-env — the same base icon per environment: prod, dev (hue-shift), staging (dot), a "#344" preview badge, a "#8790" URL-rule badge, and a custom image](docs/hero.png)
6
+
7
+ **[▶ Live, clickable demo](https://amir-abushanab.github.io/favicon-env/)**
8
+
9
+ - **Runtime mode** — one import, any framework, any deploy. Detects the environment in the browser and re-tints the favicon on a `<canvas>`, so it works with whatever favicon you already have (svg / png / ico).
10
+ - **Build-time mode** — a tiny SVG helper that bakes the tint in at build/SSR time, for zero first-paint flash.
11
+ - **Zero dependencies.** ~2 kB min+gzip (the build-time SSR helper alone is ~1.5 kB).
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ pnpm add favicon-env
17
+ ```
18
+
19
+ > **Using an AI coding agent?** favicon-env ships an [Agent Skill](https://tanstack.com/intent) (via TanStack Intent) — run `npx @tanstack/intent@latest install` and your agent picks up the correct patterns (framework placement, badges, runtime vs SSR) straight from the package, versioned with it.
20
+
21
+ ## Runtime mode
22
+
23
+ ```js
24
+ import { envFavicon } from 'favicon-env'
25
+
26
+ envFavicon({
27
+ environments: {
28
+ dev: { hue: 130 }, // hue-rotate degrees
29
+ staging: { badge: '#f59e0b' }, // …or a corner dot that keeps the logo intact
30
+ // prod omitted → left untouched
31
+ },
32
+ })
33
+ ```
34
+
35
+ Call it once on the client, as early as you can — it reads the current `<link rel="icon">`, redraws it tinted, and swaps it in. It's a no-op during SSR (it guards on `document`), so it's safe to import anywhere. Where it goes in the common setups:
36
+
37
+ <details>
38
+ <summary><b>Next.js</b> — App Router</summary>
39
+
40
+ ```tsx
41
+ // app/favicon-env.tsx — a client component
42
+ 'use client'
43
+ import { useEffect } from 'react'
44
+ import { envFavicon } from 'favicon-env'
45
+
46
+ export function FaviconEnv() {
47
+ useEffect(() => {
48
+ void envFavicon({ /* …environments, as above… */ })
49
+ }, [])
50
+ return null
51
+ }
52
+ ```
53
+
54
+ Then render `<FaviconEnv />` once inside `<body>` in `app/layout.tsx`. `useEffect` is the right hook here — a run-once, client-only side effect, safe under React StrictMode's double-invoke (re-tinting is idempotent). (Next ≥ 15.3: drop the call into `instrumentation-client.ts` and skip the component entirely. Pages Router: put the `useEffect` in `pages/_app.tsx`.)
55
+
56
+ </details>
57
+
58
+ <details>
59
+ <summary><b>TanStack Router / Start</b></summary>
60
+
61
+ ```tsx
62
+ // src/main.tsx — your client entry, before you render the router
63
+ import { envFavicon } from 'favicon-env'
64
+
65
+ void envFavicon({ /* …environments, as above… */ })
66
+ ```
67
+
68
+ The entry runs on the client, so no `useEffect` is needed. (TanStack Start / SSR: the same call in your client entry is a no-op on the server, thanks to the `document` guard.)
69
+
70
+ </details>
71
+
72
+ <details>
73
+ <summary><b>Astro</b></summary>
74
+
75
+ ```astro
76
+ ---
77
+ // src/layouts/Layout.astro — a client <script>, bundled and run in the browser
78
+ ---
79
+ <script>
80
+ import { envFavicon } from 'favicon-env'
81
+ void envFavicon({ /* …environments, as above… */ })
82
+ </script>
83
+ ```
84
+
85
+ For Astro you can also bake the tint straight into the HTML for **zero first-paint flash** — see the *Build-time / SSR mode* section below, which is the recommended path when you control the favicon SVG.
86
+
87
+ </details>
88
+
89
+ <details>
90
+ <summary><b>Vite SPA · Vue · Svelte · vanilla</b></summary>
91
+
92
+ ```js
93
+ // your client entry — src/main.ts, main.js, …
94
+ import { envFavicon } from 'favicon-env'
95
+
96
+ void envFavicon({ /* …environments, as above… */ })
97
+ ```
98
+
99
+ The entry module already runs in the browser, so no framework hook is needed. (In Vue you could equally call it from `onMounted` in your root component.)
100
+
101
+ </details>
102
+
103
+ By default the environment is guessed from the hostname (`localhost` / `*.local` / raw IPs → `dev`; a `staging`/`preview`/`qa`/… segment → `staging`; everything else → `prod`). Override it:
104
+
105
+ ```js
106
+ envFavicon({
107
+ environments: { /* … */ },
108
+ detect: () => (location.port === '4000' ? 'staging' : 'prod'),
109
+ })
110
+ ```
111
+
112
+ Environment **names are arbitrary** — they're just keys into `environments`, so you aren't limited to `dev`/`staging`/`prod`. Define your own and return them from `detect` (the built-in heuristic only emits the three defaults, so custom names need a custom `detect`):
113
+
114
+ ```js
115
+ envFavicon({
116
+ environments: {
117
+ canary: { hue: 280 },
118
+ demo: { badge: '#22c55e' },
119
+ },
120
+ detect: () => {
121
+ if (location.hostname.startsWith('canary.')) return 'canary'
122
+ if (location.hostname.endsWith('.demo.acme.com')) return 'demo'
123
+ return 'prod' // not in the map → favicon left untouched
124
+ },
125
+ })
126
+ ```
127
+
128
+ `detect` can key off anything, not just the hostname — e.g. `detect: () => import.meta.env.MODE`.
129
+
130
+ ### Auto mode
131
+
132
+ Don't want to name environments at all? Derive a **stable, unique hue from `location.host`**, so every origin *and port* automatically gets its own colour — perfect for telling several dev servers apart:
133
+
134
+ ```js
135
+ envFavicon({ auto: true })
136
+ ```
137
+
138
+ ### Badges, PR numbers & URL rules
139
+
140
+ A `badge` is either a colour (a dot) or an object with `text` — handy for preview deploys, where you want the **PR number** on the icon. The cleanest way is a `rules` list: match the URL with a `RegExp` and drop its captures straight into the text with `$1` / `$<name>`:
141
+
142
+ ```js
143
+ envFavicon({
144
+ rules: [
145
+ // e.g. a preview deploy at pr-344.myapp.dev → a "#344" pill
146
+ { match: /^pr-(\d+)\./, badge: { text: '#$1', color: '#8b5cf6' } },
147
+ { match: /staging\./, hue: 45 },
148
+ ],
149
+ })
150
+ ```
151
+
152
+ `match` is tested against `location.host` (so `:port` is included). Rules are tried in order — first match wins — then fall through to `auto` / `environments` if none match. Need more than the host? Use a function: it receives the full `URL`, and `text` can be a function too:
153
+
154
+ ```js
155
+ rules: [
156
+ {
157
+ match: (url) => url.searchParams.has('pr'),
158
+ badge: { text: (match, url) => `#${url.searchParams.get('pr')}` },
159
+ },
160
+ ]
161
+ ```
162
+
163
+ `textColor` defaults to auto (black/white by contrast with `color`).
164
+
165
+ Multi-digit numbers get cramped in a corner at 16px. `shape: 'cover'` replaces the icon with a full-bleed number so it reads even in the tab (or keep the icon and just enlarge the pill with `size` + `corner: 'center'`):
166
+
167
+ ```js
168
+ { badge: { text: '#344', color: '#8b5cf6', shape: 'cover' } }
169
+ ```
170
+
171
+ ### A different image per environment
172
+
173
+ Set `src` to swap the base image outright for an environment — e.g. a distinct staging logo. Any `hue` / `filter` / `badge` still composites on top:
174
+
175
+ ```js
176
+ envFavicon({
177
+ environments: {
178
+ staging: { src: '/favicon.staging.svg' },
179
+ preview: { src: '/favicon.svg', badge: { text: '#344' } },
180
+ },
181
+ })
182
+ ```
183
+
184
+ A plain `src` with no recolour/badge is applied directly (no canvas), so cross-origin images and crisp vectors just work.
185
+
186
+ ### No build step
187
+
188
+ Drop in a `<script>` tag; it auto-runs from `data-*` attributes and also exposes `window.faviconEnv`:
189
+
190
+ ```html
191
+ <!-- unique colour per host, zero config -->
192
+ <script src="https://unpkg.com/favicon-env/dist/favicon-env.global.js" data-auto></script>
193
+
194
+ <!-- or name your environments (hue in degrees) -->
195
+ <script
196
+ src="https://unpkg.com/favicon-env/dist/favicon-env.global.js"
197
+ data-dev="130"
198
+ data-staging="45"
199
+ ></script>
200
+ ```
201
+
202
+ ## Build-time / SSR mode
203
+
204
+ If you control the favicon SVG and want **no flash**, bake the tint in at build time instead. `faviconDataUri` returns a ready `href`:
205
+
206
+ ```astro
207
+ ---
208
+ // src/pages/index.astro
209
+ import { faviconDataUri } from 'favicon-env/ssr'
210
+ import favicon from '../favicon.svg?raw'
211
+
212
+ const env = import.meta.env.PUBLIC_APP_ENV ?? (import.meta.env.DEV ? 'dev' : 'prod')
213
+ const tint = { dev: { hue: 130 }, staging: { hue: 45 }, prod: false }[env]
214
+ ---
215
+ <link rel="icon" type="image/svg+xml" href={faviconDataUri(favicon, tint)} />
216
+ ```
217
+
218
+ `favicon-env/ssr` is pure string manipulation with no DOM dependency, so it's safe to run in Node during a build. Badges work here too — they're baked into the SVG (positioned via its `viewBox`), so you can stamp a PR number at build time with no flash:
219
+
220
+ ```js
221
+ const pr = process.env.VERCEL_GIT_PULL_REQUEST_ID
222
+ faviconDataUri(favicon, pr ? { badge: { text: `#${pr}` } } : { hue: 45 })
223
+ ```
224
+
225
+ ### Vite
226
+
227
+ A plain Vite SPA has no template to bake the tint into — its `index.html` is static. Drop this small plugin into your `vite.config` to rewrite the `<link rel="icon">` at build time, choosing the tint from Vite's `mode`:
228
+
229
+ ```js
230
+ // vite.config.js
231
+ import { readFileSync } from 'node:fs'
232
+ import path from 'node:path'
233
+ import { defineConfig } from 'vite'
234
+ import { faviconDataUri } from 'favicon-env/ssr'
235
+
236
+ // keyed by Vite `mode` (e.g. `vite build --mode staging`); omit prod to leave it untouched
237
+ const tints = {
238
+ development: { hue: 130 },
239
+ staging: { hue: 45 },
240
+ }
241
+
242
+ function faviconEnv() {
243
+ let config
244
+ return {
245
+ name: 'favicon-env',
246
+ configResolved(resolved) {
247
+ config = resolved
248
+ },
249
+ transformIndexHtml(html) {
250
+ const tint = tints[config.mode]
251
+ if (!tint) return // no rule for this mode → leave the icon alone
252
+ return html.replace(/<link\b[^>]*\brel=["']icon["'][^>]*>/i, (tag) => {
253
+ const href = tag.match(/\bhref=["']([^"']+)["']/i)?.[1]
254
+ if (!href?.endsWith('.svg')) return tag // SVG only; skip png/ico
255
+ let svg
256
+ try {
257
+ svg = readFileSync(path.join(config.publicDir, href.replace(/^\//, '')), 'utf8')
258
+ } catch {
259
+ return tag // not in public/ (missing / bundled asset) → untouched
260
+ }
261
+ return tag.replace(/\bhref=["'][^"']*["']/i, `href="${faviconDataUri(svg, tint)}"`)
262
+ })
263
+ },
264
+ }
265
+ }
266
+
267
+ export default defineConfig({
268
+ plugins: [faviconEnv()],
269
+ })
270
+ ```
271
+
272
+ Now `vite dev` and `vite build` serve the tint baked into the initial HTML — no first-paint flash — reading your favicon from `public/` and falling through untouched for non-SVG icons or a missing file. Prefer env vars to `--mode`? Swap `tints[config.mode]` for a lookup keyed off `loadEnv(config.mode, config.root, 'PUBLIC_').PUBLIC_APP_ENV`.
273
+
274
+ ## API
275
+
276
+ ### `envFavicon(options?): Promise<void>` — runtime
277
+
278
+ | option | type | default | description |
279
+ | -------------- | ------------------------------------- | -------------------- | ------------------------------------------------------------------ |
280
+ | `environments` | `Record<string, EnvTint \| false>` | — | Map of env name (any string) → tint. Missing/`false` = untouched. |
281
+ | `rules` | `EnvRule[]` | — | URL-matched tints, checked first; regex captures fill `badge.text`.|
282
+ | `detect` | `() => string \| undefined` | hostname heuristic | Return the current env name (a key of `environments`). |
283
+ | `auto` | `boolean \| { offset?: number }` | `false` | Ignore `environments`; derive a unique hue from `location.host`. |
284
+ | `source` | `string` | current icon / `.ico`| Favicon URL to tint. |
285
+ | `size` | `number` | `64` | Canvas raster size in px. |
286
+
287
+ `EnvTint`: `{ hue?: number; filter?: string; src?: string; badge?: string | Badge }`. `filter` (any CSS filter) beats `hue`; `src` replaces the base image for that env; `badge` is a colour string (a dot) or a `Badge`.
288
+
289
+ `Badge`: `{ text?: string | number; color?: string; textColor?: string; shape?: 'pill' | 'cover'; corner?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center'; size?: number; opacity?: number }`. Omit `text` for a dot; include it for a pill. `color` sets the background and `textColor` the text (default: auto black/white by contrast). Two styles: the default `'pill'` sits on top of your icon (placed by `corner`/`size`); `'cover'` replaces the whole icon with the colour + number, best for a multi-digit number that must read at 16px. `opacity` (0–1) fades the badge — with `'cover'`, below `1` it lets your icon show *through* the number (a watermark). Everything composites in one pass, so you can combine `src` + `hue`/`filter` + `badge`.
290
+
291
+ `EnvRule`: an `EnvTint` plus `match: RegExp | ((url: URL) => boolean)`. A `RegExp` is tested against `location.host` and its captures interpolate into `badge.text` (`$1`, `$<name>`); a function receives the `URL`, and in a rule `badge.text` may also be `(match, url) => string | number`.
292
+
293
+ ### `favicon-env/ssr` — build-time
294
+
295
+ - `tintSvg(svg, tint) => string` — SVG string with the tint baked in as a wrapping filtered group.
296
+ - `svgToDataUri(svg) => string` — percent-encoded `data:image/svg+xml,…`.
297
+ - `faviconDataUri(svg, tint) => string` — the two combined; a ready favicon `href`.
298
+
299
+ ### Helpers (from the main entry)
300
+
301
+ - `hashHue(input, offset?) => number` — the deterministic 0–359 hue used by auto mode.
302
+ - `defaultDetect(hostname?) => string` — the built-in `dev`/`staging`/`prod` heuristic.
303
+ - `matchRules(rules, url) => EnvTint | null` — the pure rule matcher (first match wins, captures interpolated). Reuse it server-side with a request `URL` and feed the result to `favicon-env/ssr`'s `faviconDataUri`.
304
+
305
+ ## How it works & caveats
306
+
307
+ - **Achromatic pixels barely move.** `hue-rotate` leaves white/black/grey roughly alone, so highlights and outlines survive; only the coloured parts shift.
308
+ - **Runtime + cross-origin favicons.** Tinting draws to a canvas, so a cross-origin favicon served without CORS headers taints it — `envFavicon` catches that and leaves the icon untouched. Same-origin (the normal case) is fine.
309
+ - **First-paint flash.** Runtime mode briefly shows the untinted icon before JS runs. Use the SSR helper if that matters.
310
+ - **Browser support.** Runtime mode needs canvas `ctx.filter` (Baseline; unsupported browsers just get the untinted icon). SSR mode relies on SVG favicons honouring an embedded CSS `filter`, which all current evergreen browsers do.
311
+
312
+ ## License
313
+
314
+ MIT © Amir Abushanab
@@ -0,0 +1,146 @@
1
+ // src/badge.ts
2
+ var DEFAULT_BADGE_COLOR = "#ef4444";
3
+ function normalizeBadge(badge) {
4
+ return typeof badge === "string" ? { color: badge } : badge;
5
+ }
6
+ function badgeText(badge) {
7
+ return badge.text == null ? "" : String(badge.text);
8
+ }
9
+ function placeBadge(corner, cw, ch, bw, bh, margin) {
10
+ if (corner === "center") return [(cw - bw) / 2, (ch - bh) / 2];
11
+ return [
12
+ corner.endsWith("left") ? margin : cw - bw - margin,
13
+ corner.startsWith("top") ? margin : ch - bh - margin
14
+ ];
15
+ }
16
+
17
+ // src/color.ts
18
+ function parseRgb(color) {
19
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color);
20
+ if (hex) {
21
+ const h = hex[1].length === 3 ? hex[1].replace(/./g, (c) => c + c) : hex[1];
22
+ const n = Number.parseInt(h, 16);
23
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
24
+ }
25
+ const rgb = /^rgba?\((\d+),\s*(\d+),\s*(\d+)/i.exec(color);
26
+ if (rgb) return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])];
27
+ return null;
28
+ }
29
+ function contrastColor(color) {
30
+ const rgb = parseRgb(color);
31
+ if (!rgb) return "#fff";
32
+ const luminance = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;
33
+ return luminance > 0.6 ? "#000" : "#fff";
34
+ }
35
+
36
+ // src/filter.ts
37
+ function cssFilter(tint) {
38
+ if (tint.filter) return tint.filter;
39
+ if (typeof tint.hue === "number") return `hue-rotate(${tint.hue}deg)`;
40
+ return null;
41
+ }
42
+
43
+ // src/ssr.ts
44
+ var XML_ESCAPES = {
45
+ "&": "&amp;",
46
+ "<": "&lt;",
47
+ ">": "&gt;",
48
+ '"': "&quot;",
49
+ "'": "&apos;"
50
+ };
51
+ function escapeXml(value) {
52
+ return value.replace(/[&<>"']/g, (c) => XML_ESCAPES[c] ?? c);
53
+ }
54
+ var round = (n) => Math.round(n * 100) / 100;
55
+ function opacityAttr(badge) {
56
+ return (badge.opacity ?? 1) < 1 ? ` opacity="${round(badge.opacity ?? 1)}"` : "";
57
+ }
58
+ function fitText(len) {
59
+ return ` textLength="${round(len)}" lengthAdjust="spacingAndGlyphs"`;
60
+ }
61
+ function svgText(cx, cy, fill, fontSize, fit, text) {
62
+ return `<text x="${round(cx)}" y="${round(cy)}" fill="${escapeXml(fill)}" font-family="system-ui, sans-serif" font-size="${round(fontSize)}" font-weight="700" text-anchor="middle" dominant-baseline="central"${fit}>${escapeXml(text)}</text>`;
63
+ }
64
+ function parseViewBox(openTag) {
65
+ const vb = /viewBox\s*=\s*["']([^"']+)["']/i.exec(openTag);
66
+ if (vb) {
67
+ const parts = vb[1].trim().split(/[\s,]+/).map(Number);
68
+ if (parts.length === 4 && parts.every((n) => !Number.isNaN(n))) {
69
+ return [parts[0], parts[1], parts[2], parts[3]];
70
+ }
71
+ }
72
+ const w = /\bwidth\s*=\s*["']?([\d.]+)/i.exec(openTag);
73
+ const h = /\bheight\s*=\s*["']?([\d.]+)/i.exec(openTag);
74
+ if (w && h) return [0, 0, Number(w[1]), Number(h[1])];
75
+ return null;
76
+ }
77
+ function svgBadge([minX, minY, w, h], badge) {
78
+ const text = badgeText(badge);
79
+ const color = badge.color ?? DEFAULT_BADGE_COLOR;
80
+ const corner = badge.corner ?? "bottom-right";
81
+ const bh = h * (badge.size ?? 0.5);
82
+ const fontSize = bh * 0.62;
83
+ const margin = w * 0.02;
84
+ const natural = text ? Math.max(bh, text.length * fontSize * 0.62 + bh * 0.5) : bh;
85
+ const bw = text ? Math.min(natural, w - margin * 2) : bh;
86
+ const [px, py] = placeBadge(corner, w, h, bw, bh, margin);
87
+ const x = minX + px;
88
+ const y = minY + py;
89
+ const rx = text ? Math.min(bh / 2, w * 0.24) : bh / 2;
90
+ const fit = bw < natural ? fitText(bw - bh * 0.4) : "";
91
+ const label = text ? svgText(x + bw / 2, y + bh / 2, badge.textColor ?? contrastColor(color), fontSize, fit, text) : "";
92
+ return `<g${opacityAttr(badge)}><rect x="${round(x)}" y="${round(y)}" width="${round(bw)}" height="${round(bh)}" rx="${round(rx)}" fill="${escapeXml(color)}" stroke="rgba(0,0,0,0.35)" stroke-width="${round(h * 0.015)}"/>${label}</g>`;
93
+ }
94
+ function svgCover([minX, minY, w, h], badge) {
95
+ const color = badge.color ?? DEFAULT_BADGE_COLOR;
96
+ const text = badgeText(badge);
97
+ const side = Math.min(w, h);
98
+ const op = opacityAttr(badge);
99
+ const rect = `<rect x="${round(minX)}" y="${round(minY)}" width="${round(w)}" height="${round(h)}" rx="${round(side * 0.2)}" fill="${escapeXml(color)}"/>`;
100
+ if (!text) return `<g${op}>${rect}</g>`;
101
+ const fontSize = side * 0.62;
102
+ const maxLen = w * 0.84;
103
+ const fit = text.length * fontSize * 0.62 > maxLen ? fitText(maxLen) : "";
104
+ const label = svgText(
105
+ minX + w / 2,
106
+ minY + h / 2,
107
+ badge.textColor ?? contrastColor(color),
108
+ fontSize,
109
+ fit,
110
+ text
111
+ );
112
+ return `<g${op}>${rect}${label}</g>`;
113
+ }
114
+ function tintSvg(svg, tint) {
115
+ if (!tint) return svg;
116
+ const filter = cssFilter(tint);
117
+ const badge = tint.badge ? normalizeBadge(tint.badge) : null;
118
+ if (!filter && !badge) return svg;
119
+ const open = /<svg\b[^>]*>/i.exec(svg);
120
+ if (!open) return svg;
121
+ const close = svg.lastIndexOf("</svg>");
122
+ if (close === -1) return svg;
123
+ const openEnd = open.index + open[0].length;
124
+ if (badge?.shape === "cover") {
125
+ const vb = parseViewBox(open[0]);
126
+ if (!vb) return svg;
127
+ const base = (badge.opacity ?? 1) < 1 ? svg.slice(openEnd, close) : "";
128
+ return `${svg.slice(0, openEnd)}${base}${svgCover(vb, badge)}${svg.slice(close)}`;
129
+ }
130
+ const inner = svg.slice(openEnd, close);
131
+ const style = filter ? `<style>.__favenv{filter:${filter}}</style>` : "";
132
+ const body = filter ? `<g class="__favenv">${inner}</g>` : inner;
133
+ const viewBox = badge ? parseViewBox(open[0]) : null;
134
+ const badgeSvg = badge && viewBox ? svgBadge(viewBox, badge) : "";
135
+ return `${svg.slice(0, openEnd)}${style}${body}${badgeSvg}${svg.slice(close)}`;
136
+ }
137
+ function svgToDataUri(svg) {
138
+ return `data:image/svg+xml,${encodeURIComponent(svg)}`;
139
+ }
140
+ function faviconDataUri(svg, tint) {
141
+ return svgToDataUri(tintSvg(svg, tint));
142
+ }
143
+
144
+ export { DEFAULT_BADGE_COLOR, badgeText, contrastColor, cssFilter, faviconDataUri, normalizeBadge, placeBadge, svgToDataUri, tintSvg };
145
+ //# sourceMappingURL=chunk-NRMMGQRU.js.map
146
+ //# sourceMappingURL=chunk-NRMMGQRU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/badge.ts","../src/color.ts","../src/filter.ts","../src/ssr.ts"],"names":[],"mappings":"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","file":"chunk-NRMMGQRU.js","sourcesContent":["import type { Badge, BadgeCorner } from './types';\n\n/** Default badge background — the red dot/pill you get when no `color` is set. */\nexport const DEFAULT_BADGE_COLOR = '#ef4444';\n\n/** Normalise the `badge` shorthand (a `string` is a dot of that colour) to a `Badge`. */\nexport function normalizeBadge(badge: string | Badge): Badge {\n return typeof badge === 'string' ? { color: badge } : badge;\n}\n\n/** A badge's text as a string (`''` when omitted) — so a `0` renders and `undefined` doesn't. */\nexport function badgeText(badge: Badge): string {\n return badge.text == null ? '' : String(badge.text);\n}\n\n/**\n * Top-left corner for a `bw`×`bh` badge inside a `cw`×`ch` box, per `corner`\n * (relative to the box origin; the caller adds any offset). Shared by the canvas\n * and SVG renderers so placement stays identical across runtime and build-time.\n */\nexport function placeBadge(\n corner: BadgeCorner,\n cw: number,\n ch: number,\n bw: number,\n bh: number,\n margin: number,\n): [number, number] {\n if (corner === 'center') return [(cw - bw) / 2, (ch - bh) / 2];\n return [\n corner.endsWith('left') ? margin : cw - bw - margin,\n corner.startsWith('top') ? margin : ch - bh - margin,\n ];\n}\n","/** Parse `#rgb` / `#rrggbb` / `rgb(…)` / `rgba(…)` into `[r, g, b]` (0–255), or `null`. */\nfunction parseRgb(color: string): [number, number, number] | null {\n const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color);\n if (hex) {\n const h = hex[1].length === 3 ? hex[1].replace(/./g, (c) => c + c) : hex[1];\n const n = Number.parseInt(h, 16);\n return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n }\n const rgb = /^rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/i.exec(color);\n if (rgb) return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])];\n return null;\n}\n\n/**\n * Black or white — whichever reads better on `color` — by perceived luminance.\n * Falls back to white for colours it can't parse (named / `hsl()` / etc.).\n */\nexport function contrastColor(color: string): string {\n const rgb = parseRgb(color);\n if (!rgb) return '#fff';\n const luminance = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;\n return luminance > 0.6 ? '#000' : '#fff';\n}\n","import type { EnvTint } from './types';\n\n/** The CSS `filter` for a tint — an explicit `filter` beats `hue`; `null` if neither is set. */\nexport function cssFilter(tint: EnvTint): string | null {\n if (tint.filter) return tint.filter;\n if (typeof tint.hue === 'number') return `hue-rotate(${tint.hue}deg)`;\n return null;\n}\n","import { badgeText, DEFAULT_BADGE_COLOR, normalizeBadge, placeBadge } from './badge';\nimport { contrastColor } from './color';\nimport { cssFilter } from './filter';\nimport type { Badge, EnvConfig } from './types';\n\nconst XML_ESCAPES: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&apos;',\n};\n\nfunction escapeXml(value: string): string {\n return value.replace(/[&<>\"']/g, (c) => XML_ESCAPES[c] ?? c);\n}\n\nconst round = (n: number): number => Math.round(n * 100) / 100;\n\n/** ` opacity=\"…\"` attribute for a translucent badge group, else `''`. */\nfunction opacityAttr(badge: Badge): string {\n return (badge.opacity ?? 1) < 1 ? ` opacity=\"${round(badge.opacity ?? 1)}\"` : '';\n}\n\n/** Attributes that squeeze the glyphs into `len` when the natural text would overflow. */\nfunction fitText(len: number): string {\n return ` textLength=\"${round(len)}\" lengthAdjust=\"spacingAndGlyphs\"`;\n}\n\n/** A centred, bold `<text>` element at (`cx`, `cy`) — the one badge-label form. */\nfunction svgText(\n cx: number,\n cy: number,\n fill: string,\n fontSize: number,\n fit: string,\n text: string,\n): string {\n return (\n `<text x=\"${round(cx)}\" y=\"${round(cy)}\" fill=\"${escapeXml(fill)}\" ` +\n `font-family=\"system-ui, sans-serif\" font-size=\"${round(fontSize)}\" font-weight=\"700\" ` +\n `text-anchor=\"middle\" dominant-baseline=\"central\"${fit}>${escapeXml(text)}</text>`\n );\n}\n\n/** Read the drawable box from a root `<svg>` tag: its `viewBox`, else `width`/`height`. */\nfunction parseViewBox(openTag: string): [number, number, number, number] | null {\n const vb = /viewBox\\s*=\\s*[\"']([^\"']+)[\"']/i.exec(openTag);\n if (vb) {\n const parts = vb[1]\n .trim()\n .split(/[\\s,]+/)\n .map(Number);\n if (parts.length === 4 && parts.every((n) => !Number.isNaN(n))) {\n return [parts[0], parts[1], parts[2], parts[3]];\n }\n }\n const w = /\\bwidth\\s*=\\s*[\"']?([\\d.]+)/i.exec(openTag);\n const h = /\\bheight\\s*=\\s*[\"']?([\\d.]+)/i.exec(openTag);\n if (w && h) return [0, 0, Number(w[1]), Number(h[1])];\n return null;\n}\n\n/** Build an SVG `<g>` badge (dot, or a pill with text) sized to the viewBox. */\nfunction svgBadge([minX, minY, w, h]: [number, number, number, number], badge: Badge): string {\n const text = badgeText(badge);\n const color = badge.color ?? DEFAULT_BADGE_COLOR;\n const corner = badge.corner ?? 'bottom-right';\n const bh = h * (badge.size ?? 0.5);\n const fontSize = bh * 0.62;\n const margin = w * 0.02;\n // SVG has no text metrics at build time; approximate glyph width at ~0.62em,\n // then clamp to the icon so a long label (e.g. a big PR number) can't overflow.\n const natural = text ? Math.max(bh, text.length * fontSize * 0.62 + bh * 0.5) : bh;\n const bw = text ? Math.min(natural, w - margin * 2) : bh;\n const [px, py] = placeBadge(corner, w, h, bw, bh, margin);\n const x = minX + px;\n const y = minY + py;\n const rx = text ? Math.min(bh / 2, w * 0.24) : bh / 2;\n // If clamped, force the glyphs to fit the pill width.\n const fit = bw < natural ? fitText(bw - bh * 0.4) : '';\n const label = text\n ? svgText(x + bw / 2, y + bh / 2, badge.textColor ?? contrastColor(color), fontSize, fit, text)\n : '';\n return (\n `<g${opacityAttr(badge)}><rect x=\"${round(x)}\" y=\"${round(y)}\" width=\"${round(bw)}\" height=\"${round(bh)}\" ` +\n `rx=\"${round(rx)}\" fill=\"${escapeXml(color)}\" stroke=\"rgba(0,0,0,0.35)\" ` +\n `stroke-width=\"${round(h * 0.015)}\"/>${label}</g>`\n );\n}\n\n/** Build a full-icon \"cover\" tile — a background rect plus a big centred number. */\nfunction svgCover([minX, minY, w, h]: [number, number, number, number], badge: Badge): string {\n const color = badge.color ?? DEFAULT_BADGE_COLOR;\n const text = badgeText(badge);\n const side = Math.min(w, h);\n const op = opacityAttr(badge);\n const rect =\n `<rect x=\"${round(minX)}\" y=\"${round(minY)}\" width=\"${round(w)}\" height=\"${round(h)}\" ` +\n `rx=\"${round(side * 0.2)}\" fill=\"${escapeXml(color)}\"/>`;\n if (!text) return `<g${op}>${rect}</g>`;\n const fontSize = side * 0.62;\n const maxLen = w * 0.84;\n const fit = text.length * fontSize * 0.62 > maxLen ? fitText(maxLen) : '';\n const label = svgText(\n minX + w / 2,\n minY + h / 2,\n badge.textColor ?? contrastColor(color),\n fontSize,\n fit,\n text,\n );\n return `<g${op}>${rect}${label}</g>`;\n}\n\n/**\n * Return `svg` (an SVG *string*) with the environment's tint and/or badge baked\n * in — the form that survives being rendered as an `<img>` / favicon, with no\n * first-paint flash. The tint is a CSS `filter` on a wrapping group; the badge\n * is an appended `<g>` positioned via the SVG's `viewBox` (a badge is skipped if\n * no `viewBox` or `width`/`height` can be read).\n *\n * Returns the SVG unchanged when `tint` is falsy, has nothing to apply, or the\n * input isn't a recognisable `<svg>…</svg>` document.\n */\nexport function tintSvg(svg: string, tint: EnvConfig): string {\n if (!tint) return svg;\n const filter = cssFilter(tint);\n const badge = tint.badge ? normalizeBadge(tint.badge) : null;\n if (!filter && !badge) return svg;\n const open = /<svg\\b[^>]*>/i.exec(svg);\n if (!open) return svg;\n const close = svg.lastIndexOf('</svg>');\n if (close === -1) return svg;\n const openEnd = open.index + open[0].length;\n\n // `shape: 'cover'` replaces the icon's content with a full-bleed number tile.\n // A translucent cover (opacity < 1) keeps the base showing through instead.\n if (badge?.shape === 'cover') {\n const vb = parseViewBox(open[0]);\n if (!vb) return svg;\n const base = (badge.opacity ?? 1) < 1 ? svg.slice(openEnd, close) : '';\n return `${svg.slice(0, openEnd)}${base}${svgCover(vb, badge)}${svg.slice(close)}`;\n }\n\n const inner = svg.slice(openEnd, close);\n // No XML comments injected here — XML comments may not contain `--`, which\n // every `--custom-property` does, and that silently breaks favicon SVGs.\n const style = filter ? `<style>.__favenv{filter:${filter}}</style>` : '';\n const body = filter ? `<g class=\"__favenv\">${inner}</g>` : inner;\n const viewBox = badge ? parseViewBox(open[0]) : null;\n const badgeSvg = badge && viewBox ? svgBadge(viewBox, badge) : '';\n return `${svg.slice(0, openEnd)}${style}${body}${badgeSvg}${svg.slice(close)}`;\n}\n\n/** Percent-encode an SVG string as a `data:` URI suitable for a favicon `href`. */\nexport function svgToDataUri(svg: string): string {\n return `data:image/svg+xml,${encodeURIComponent(svg)}`;\n}\n\n/**\n * Convenience: `tintSvg` + `svgToDataUri`. Give it your favicon SVG and the\n * config for the current build's environment; get back a ready-to-use\n * `<link rel=\"icon\" href=\"…\">` value with no first-paint flash.\n */\nexport function faviconDataUri(svg: string, tint: EnvConfig): string {\n return svgToDataUri(tintSvg(svg, tint));\n}\n"]}
@@ -0,0 +1,2 @@
1
+ "use strict";var faviconEnv=(()=>{var p=Object.defineProperty;var P=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var H=(e,t)=>{for(var n in t)p(e,n,{get:t[n],enumerable:!0})},_=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of w(t))!I.call(e,o)&&o!==n&&p(e,o,{get:()=>t[o],enumerable:!(r=P(t,o))||r.enumerable});return e};var j=e=>_(p({},"__esModule",{value:!0}),e);var Z={};H(Z,{envFavicon:()=>g});var T="#ef4444";function M(e){return typeof e=="string"?{color:e}:e}function R(e){return e.text==null?"":String(e.text)}function x(e,t,n,r,o,i){return e==="center"?[(t-r)/2,(n-o)/2]:[e.endsWith("left")?i:t-r-i,e.startsWith("top")?i:n-o-i]}function q(e){let t=/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(e);if(t){let r=t[1].length===3?t[1].replace(/./g,i=>i+i):t[1],o=Number.parseInt(r,16);return[o>>16&255,o>>8&255,o&255]}let n=/^rgba?\((\d+),\s*(\d+),\s*(\d+)/i.exec(e);return n?[Number(n[1]),Number(n[2]),Number(n[3])]:null}function B(e){let t=q(e);return t&&(.299*t[0]+.587*t[1]+.114*t[2])/255>.6?"#000":"#fff"}function C(e=typeof location>"u"?"":location.hostname){let t=e.toLowerCase();return t==="localhost"||t==="127.0.0.1"||t==="::1"||t==="[::1]"||t.endsWith(".local")||t.endsWith(".localhost")||/^\d{1,3}(?:\.\d{1,3}){3}$/.test(t)?"dev":/(?:^|[.-])(?:staging|stg|qa|uat|preview|preprod|test|dev)(?:[.-]|$)/.test(t)?"staging":"prod"}function L(e){return e.filter?e.filter:typeof e.hue=="number"?`hue-rotate(${e.hue}deg)`:null}function S(e,t=0){let n=2166136261;for(let r=0;r<e.length;r+=1)n^=e.charCodeAt(r),n=Math.imul(n,16777619);return((n>>>0)%360+t%360+360)%360}var N='link[rel~="icon"]',$;function G(){let e=document.querySelectorAll(N);return e.item(e.length-1)?.href||void 0}function K(e,t){return e.replace(/\$(?:(\d+)|<([^>]+)>)/g,(n,r,o)=>o==null?t[Number(r)]??"":t.groups?.[o]??"")}function J(e,t,n){if(typeof e=="string")return{color:e};let{text:r,...o}=e,i;return typeof r=="function"?i=r(t,n):typeof r=="string"&&t?i=K(r,t):i=r,{...o,text:i}}function A(e,t,n){let r={};return e.hue!=null&&(r.hue=e.hue),e.filter&&(r.filter=e.filter),e.src&&(r.src=e.src),e.badge!=null&&(r.badge=J(e.badge,t,n)),r}function Q(e,t){for(let n of e)if(typeof n.match=="function"){if(n.match(t))return A(n,null,t)}else{let r=t.host.match(n.match);if(r)return A(n,r,t)}return null}function V(e){let t=typeof location>"u"?null:new URL(location.href);if(e.rules&&t){let o=Q(e.rules,t);if(o)return o}if(e.auto){let o=typeof e.auto=="object"?e.auto.offset??0:0;return{hue:S(t?.host??"",o)}}let n=(e.detect??C)();return(n?e.environments?.[n]:void 0)||null}function X(e){if(e.startsWith("data:"))return/^data:([^;,]+)/.exec(e)?.[1];if(/\.svg(?:[?#]|$)/i.test(e))return"image/svg+xml";if(/\.png(?:[?#]|$)/i.test(e))return"image/png";if(/\.ico(?:[?#]|$)/i.test(e))return"image/x-icon"}function h(e,t){document.querySelectorAll(N).forEach(r=>r.remove());let n=document.createElement("link");n.rel="icon",t&&(n.type=t),n.href=e,n.dataset.faviconEnv="",document.head.append(n)}function O(e,t){return e.fillStyle=t,B(e.fillStyle)}function F(e,t,n,r,o,i){let c=Math.min(i,r/2,o/2);e.beginPath(),e.moveTo(t+c,n),e.arcTo(t+r,n,t+r,n+o,c),e.arcTo(t+r,n+o,t,n+o,c),e.arcTo(t,n+o,t,n,c),e.arcTo(t,n,t+r,n,c),e.closePath()}function D(e,t,n){let r=n.color??T,o=R(n),i='system-ui, -apple-system, "Segoe UI", sans-serif';if(n.shape==="cover"){if(F(e,0,0,t,t,Math.round(t*.2)),e.fillStyle=r,e.fill(),o){let b=t*.84,m=Math.round(t*.62);e.font=`700 ${m}px ${i}`;let E=e.measureText(o).width;E>b&&(m=Math.max(7,Math.floor(m*(b/E))),e.font=`700 ${m}px ${i}`),e.fillStyle=n.textColor??O(e,r),e.textAlign="center",e.textBaseline="middle",e.fillText(o,t/2,t/2)}return}let c=n.corner??"bottom-right",s=Math.round(t*(n.size??.5)),l=Math.round(t*.02),a=t-l*2,d=Math.round(s*.5),u=Math.round(s*.66);e.font=`700 ${u}px ${i}`;let f=o?Math.max(s,Math.ceil(e.measureText(o).width)+d):s;f>a&&(u=Math.max(7,Math.floor(u*(a/f))),e.font=`700 ${u}px ${i}`,f=Math.min(a,Math.max(s,Math.ceil(e.measureText(o).width)+d)));let[U,W]=x(c,t,t,f,s,l),v=Math.round(U),y=Math.round(W),k=o?Math.min(s/2,t*.24):s/2;F(e,v,y,f,s,k),e.fillStyle=r,e.fill(),e.lineWidth=Math.max(1,t*.03),e.strokeStyle="rgba(0, 0, 0, 0.35)",e.stroke(),o&&(e.fillStyle=n.textColor??O(e,r),e.textAlign="center",e.textBaseline="middle",e.fillText(o,v+f/2,y+s/2))}function g(e={}){if(typeof document>"u"||typeof HTMLCanvasElement>"u")return Promise.resolve();let t=V(e);if(!t)return Promise.resolve();let n=t.badge!=null?M(t.badge):void 0,r=e.size??64,o=n?.shape==="cover",i=n?.opacity??1;if(o&&i>=1){let l=document.createElement("canvas");l.width=r,l.height=r;let a=l.getContext("2d");return a&&(D(a,r,n),h(l.toDataURL("image/png"),"image/png")),Promise.resolve()}if(!(t.hue!=null||!!t.filter||!!n))return t.src&&h(t.src,X(t.src)),Promise.resolve();$??($=G());let s=t.src??e.source??$??"/favicon.ico";return new Promise(l=>{let a=new Image;a.crossOrigin="anonymous",a.decoding="async",a.addEventListener("error",()=>l()),a.addEventListener("load",()=>{try{let d=document.createElement("canvas");d.width=r,d.height=r;let u=d.getContext("2d");if(u){if(!o){let f=L(t);f&&(u.filter=f)}u.drawImage(a,0,0,r,r),u.filter="none",n&&(u.globalAlpha=i,D(u,r,n),u.globalAlpha=1),h(d.toDataURL("image/png"),"image/png")}}catch{}l()}),a.src=s})}function Y(){let e=document.currentScript;if(!e)return;let t=e.dataset;if(t.auto!==void 0){g({auto:!0});return}let n={};for(let[r,o]of Object.entries(t)){let i=Number(o);o!==void 0&&o!==""&&!Number.isNaN(i)&&(n[r]={hue:i})}Object.keys(n).length>0&&g({environments:n})}Y();return j(Z);})();
2
+ //# sourceMappingURL=favicon-env.global.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/global.ts","../src/badge.ts","../src/color.ts","../src/detect.ts","../src/filter.ts","../src/hash.ts","../src/tint.ts"],"sourcesContent":["import { envFavicon } from './tint';\nimport type { EnvFaviconOptions, EnvTint } from './types';\n\n// IIFE entry for no-build sites. Exposed as `window.faviconEnv.envFavicon(...)`,\n// and auto-runs from the loading <script>'s data-* attributes:\n// <script src=\"…/favicon-env.global.js\" data-auto></script>\n// <script src=\"…/favicon-env.global.js\" data-dev=\"130\" data-staging=\"45\"></script>\nfunction boot(): void {\n const el = document.currentScript as HTMLScriptElement | null;\n if (!el) return;\n const data = el.dataset;\n if (data.auto !== undefined) {\n void envFavicon({ auto: true });\n return;\n }\n const environments: Record<string, EnvTint> = {};\n for (const [name, value] of Object.entries(data)) {\n const hue = Number(value);\n if (value !== undefined && value !== '' && !Number.isNaN(hue)) {\n environments[name] = { hue };\n }\n }\n if (Object.keys(environments).length > 0) void envFavicon({ environments });\n}\n\nboot();\n\nexport { envFavicon };\nexport type { EnvFaviconOptions };\n","import type { Badge, BadgeCorner } from './types';\n\n/** Default badge background — the red dot/pill you get when no `color` is set. */\nexport const DEFAULT_BADGE_COLOR = '#ef4444';\n\n/** Normalise the `badge` shorthand (a `string` is a dot of that colour) to a `Badge`. */\nexport function normalizeBadge(badge: string | Badge): Badge {\n return typeof badge === 'string' ? { color: badge } : badge;\n}\n\n/** A badge's text as a string (`''` when omitted) — so a `0` renders and `undefined` doesn't. */\nexport function badgeText(badge: Badge): string {\n return badge.text == null ? '' : String(badge.text);\n}\n\n/**\n * Top-left corner for a `bw`×`bh` badge inside a `cw`×`ch` box, per `corner`\n * (relative to the box origin; the caller adds any offset). Shared by the canvas\n * and SVG renderers so placement stays identical across runtime and build-time.\n */\nexport function placeBadge(\n corner: BadgeCorner,\n cw: number,\n ch: number,\n bw: number,\n bh: number,\n margin: number,\n): [number, number] {\n if (corner === 'center') return [(cw - bw) / 2, (ch - bh) / 2];\n return [\n corner.endsWith('left') ? margin : cw - bw - margin,\n corner.startsWith('top') ? margin : ch - bh - margin,\n ];\n}\n","/** Parse `#rgb` / `#rrggbb` / `rgb(…)` / `rgba(…)` into `[r, g, b]` (0–255), or `null`. */\nfunction parseRgb(color: string): [number, number, number] | null {\n const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color);\n if (hex) {\n const h = hex[1].length === 3 ? hex[1].replace(/./g, (c) => c + c) : hex[1];\n const n = Number.parseInt(h, 16);\n return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n }\n const rgb = /^rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/i.exec(color);\n if (rgb) return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])];\n return null;\n}\n\n/**\n * Black or white — whichever reads better on `color` — by perceived luminance.\n * Falls back to white for colours it can't parse (named / `hsl()` / etc.).\n */\nexport function contrastColor(color: string): string {\n const rgb = parseRgb(color);\n if (!rgb) return '#fff';\n const luminance = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;\n return luminance > 0.6 ? '#000' : '#fff';\n}\n","/**\n * Default environment heuristic, based on `location.hostname`:\n *\n * - `localhost`, `127.0.0.1`, `::1`, `*.local`, `*.localhost`, a raw IPv4 → `'dev'`\n * - a `staging` / `stg` / `qa` / `uat` / `preview` / `preprod` / `test` / `dev`\n * segment anywhere in the host → `'staging'`\n * - anything else → `'prod'`\n *\n * It's intentionally simple. Pass your own `detect` to `envFavicon` when your\n * hostnames don't follow these conventions.\n *\n * @param hostname override the host to classify (defaults to `location.hostname`)\n */\nexport function defaultDetect(\n hostname: string = typeof location === 'undefined' ? '' : location.hostname,\n): string {\n const h = hostname.toLowerCase();\n if (\n h === 'localhost' ||\n h === '127.0.0.1' ||\n h === '::1' ||\n h === '[::1]' ||\n h.endsWith('.local') ||\n h.endsWith('.localhost') ||\n /^\\d{1,3}(?:\\.\\d{1,3}){3}$/.test(h)\n ) {\n return 'dev';\n }\n if (/(?:^|[.-])(?:staging|stg|qa|uat|preview|preprod|test|dev)(?:[.-]|$)/.test(h)) {\n return 'staging';\n }\n return 'prod';\n}\n","import type { EnvTint } from './types';\n\n/** The CSS `filter` for a tint — an explicit `filter` beats `hue`; `null` if neither is set. */\nexport function cssFilter(tint: EnvTint): string | null {\n if (tint.filter) return tint.filter;\n if (typeof tint.hue === 'number') return `hue-rotate(${tint.hue}deg)`;\n return null;\n}\n","/**\n * Deterministic hue (0–359) derived from a string via FNV-1a. Stable across\n * loads and well distributed, so distinct inputs (e.g. `location.host` values)\n * get distinct colours. Powers `envFavicon`'s auto mode.\n *\n * @param input the string to hash (e.g. `location.host`)\n * @param offset extra degrees added to the result, to shift the whole palette\n */\nexport function hashHue(input: string, offset = 0): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i += 1) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (((h >>> 0) % 360) + (offset % 360) + 360) % 360;\n}\n","import { badgeText, DEFAULT_BADGE_COLOR, normalizeBadge, placeBadge } from './badge';\nimport { contrastColor } from './color';\nimport { defaultDetect } from './detect';\nimport { cssFilter } from './filter';\nimport { hashHue } from './hash';\nimport type { Badge, EnvFaviconOptions, EnvRule, EnvTint, RuleBadge } from './types';\n\n/** Selector for the favicon `<link>`(s) we read from and replace. */\nconst ICON_LINK = 'link[rel~=\"icon\"]';\n\n/** Remembered original favicon href, so repeat calls don't tint an already-tinted icon. */\nlet originalSource: string | undefined;\n\nfunction currentIconHref(): string | undefined {\n const links = document.querySelectorAll<HTMLLinkElement>(ICON_LINK);\n const last = links.item(links.length - 1);\n return last?.href || undefined;\n}\n\n/** Fill `$1` / `$<name>` placeholders in a template from a regex match. */\nfunction interpolate(template: string, match: RegExpMatchArray): string {\n return template.replace(/\\$(?:(\\d+)|<([^>]+)>)/g, (_full, index, name) =>\n name == null ? (match[Number(index)] ?? '') : (match.groups?.[name] ?? ''),\n );\n}\n\nfunction resolveRuleBadge(\n badge: string | RuleBadge,\n match: RegExpMatchArray | null,\n url: URL,\n): Badge {\n if (typeof badge === 'string') return { color: badge };\n const { text, ...rest } = badge;\n let resolved: string | number | undefined;\n if (typeof text === 'function') resolved = text(match, url);\n else if (typeof text === 'string' && match) resolved = interpolate(text, match);\n else resolved = text;\n return { ...rest, text: resolved };\n}\n\nfunction ruleToTint(rule: EnvRule, match: RegExpMatchArray | null, url: URL): EnvTint {\n const tint: EnvTint = {};\n if (rule.hue != null) tint.hue = rule.hue;\n if (rule.filter) tint.filter = rule.filter;\n if (rule.src) tint.src = rule.src;\n if (rule.badge != null) tint.badge = resolveRuleBadge(rule.badge, match, url);\n return tint;\n}\n\n/**\n * Return the tint of the first `rule` whose `match` matches `url`, or `null` if\n * none do. A `RegExp` `match` is tested against `url.host` and its captures are\n * interpolated into `badge.text` (`$1`, `$<name>`); a function `match` receives\n * the `URL`. Exposed so the same rules can drive server-side rendering from a\n * request URL — pair the result with `favicon-env/ssr`'s `faviconDataUri`.\n */\nexport function matchRules(rules: EnvRule[], url: URL): EnvTint | null {\n for (const rule of rules) {\n if (typeof rule.match === 'function') {\n if (rule.match(url)) return ruleToTint(rule, null, url);\n } else {\n const m = url.host.match(rule.match);\n if (m) return ruleToTint(rule, m, url);\n }\n }\n return null;\n}\n\nfunction resolveTint(options: EnvFaviconOptions): EnvTint | null {\n const url = typeof location === 'undefined' ? null : new URL(location.href);\n if (options.rules && url) {\n const matched = matchRules(options.rules, url);\n if (matched) return matched;\n }\n if (options.auto) {\n const offset = typeof options.auto === 'object' ? (options.auto.offset ?? 0) : 0;\n return { hue: hashHue(url?.host ?? '', offset) };\n }\n const env = (options.detect ?? defaultDetect)();\n const tint = env ? options.environments?.[env] : undefined;\n return tint || null;\n}\n\nfunction inferType(src: string): string | undefined {\n if (src.startsWith('data:')) return /^data:([^;,]+)/.exec(src)?.[1];\n if (/\\.svg(?:[?#]|$)/i.test(src)) return 'image/svg+xml';\n if (/\\.png(?:[?#]|$)/i.test(src)) return 'image/png';\n if (/\\.ico(?:[?#]|$)/i.test(src)) return 'image/x-icon';\n return undefined;\n}\n\nfunction applyFavicon(href: string, type?: string): void {\n document.querySelectorAll(ICON_LINK).forEach((link) => link.remove());\n const link = document.createElement('link');\n link.rel = 'icon';\n if (type) link.type = type;\n link.href = href;\n link.dataset.faviconEnv = '';\n document.head.append(link);\n}\n\nfunction contrastText(ctx: CanvasRenderingContext2D, background: string): string {\n ctx.fillStyle = background; // canvas normalises any CSS colour to #rrggbb / rgba(…)\n return contrastColor(ctx.fillStyle);\n}\n\nfunction traceRoundRect(\n ctx: CanvasRenderingContext2D,\n x: number,\n y: number,\n w: number,\n h: number,\n r: number,\n): void {\n const rr = Math.min(r, w / 2, h / 2);\n ctx.beginPath();\n ctx.moveTo(x + rr, y);\n ctx.arcTo(x + w, y, x + w, y + h, rr);\n ctx.arcTo(x + w, y + h, x, y + h, rr);\n ctx.arcTo(x, y + h, x, y, rr);\n ctx.arcTo(x, y, x + w, y, rr);\n ctx.closePath();\n}\n\nfunction drawBadge(ctx: CanvasRenderingContext2D, size: number, badge: Badge): void {\n const color = badge.color ?? DEFAULT_BADGE_COLOR;\n const text = badgeText(badge);\n const family = 'system-ui, -apple-system, \"Segoe UI\", sans-serif';\n\n // `shape: 'cover'` fills the whole icon and centres the number as big as it fits.\n if (badge.shape === 'cover') {\n traceRoundRect(ctx, 0, 0, size, size, Math.round(size * 0.2));\n ctx.fillStyle = color;\n ctx.fill();\n if (text) {\n const maxWidth = size * 0.84;\n let fontSize = Math.round(size * 0.62);\n ctx.font = `700 ${fontSize}px ${family}`;\n const textWidth = ctx.measureText(text).width;\n if (textWidth > maxWidth) {\n fontSize = Math.max(7, Math.floor(fontSize * (maxWidth / textWidth)));\n ctx.font = `700 ${fontSize}px ${family}`;\n }\n ctx.fillStyle = badge.textColor ?? contrastText(ctx, color);\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(text, size / 2, size / 2);\n }\n return;\n }\n\n const corner = badge.corner ?? 'bottom-right';\n const h = Math.round(size * (badge.size ?? 0.5));\n const margin = Math.round(size * 0.02);\n const maxWidth = size - margin * 2;\n const pad = Math.round(h * 0.5);\n // A dot / short label sits in a pill that widens (then the font shrinks) to fit\n // longer text. Bump badge.size + corner:'center' to make a number dominate.\n let fontSize = Math.round(h * 0.66);\n ctx.font = `700 ${fontSize}px ${family}`;\n let w = text ? Math.max(h, Math.ceil(ctx.measureText(text).width) + pad) : h;\n if (w > maxWidth) {\n fontSize = Math.max(7, Math.floor(fontSize * (maxWidth / w)));\n ctx.font = `700 ${fontSize}px ${family}`;\n w = Math.min(maxWidth, Math.max(h, Math.ceil(ctx.measureText(text).width) + pad));\n }\n const [px, py] = placeBadge(corner, size, size, w, h, margin);\n const x = Math.round(px);\n const y = Math.round(py);\n const radius = text ? Math.min(h / 2, size * 0.24) : h / 2;\n\n traceRoundRect(ctx, x, y, w, h, radius);\n ctx.fillStyle = color;\n ctx.fill();\n ctx.lineWidth = Math.max(1, size * 0.03);\n ctx.strokeStyle = 'rgba(0, 0, 0, 0.35)';\n ctx.stroke();\n\n if (text) {\n ctx.fillStyle = badge.textColor ?? contrastText(ctx, color);\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(text, x + w / 2, y + h / 2);\n }\n}\n\n/**\n * Tint / decorate the page favicon for the current environment. Resolves a tint\n * from `rules` (URL match) → `auto` → `environments`/`detect`, then reads the\n * existing `<link rel=\"icon\">` (or a per-env `src` / `options.source`), redraws\n * it on a `<canvas>` with the hue / filter / badge, and swaps in the result as a\n * PNG data URL.\n *\n * A no-op during SSR (no `document`) or when nothing resolves. Works with any\n * favicon format (svg / png / ico). A custom `src` with no recolour or badge is\n * swapped in directly, skipping the canvas — which avoids cross-origin taint and\n * keeps vector sources sharp. If a canvas source is cross-origin without CORS\n * headers the canvas is tainted and the favicon is left untouched.\n *\n * @returns a promise that resolves once the swap has been attempted.\n */\nexport function envFavicon(options: EnvFaviconOptions = {}): Promise<void> {\n if (typeof document === 'undefined' || typeof HTMLCanvasElement === 'undefined') {\n return Promise.resolve();\n }\n const tint = resolveTint(options);\n if (!tint) return Promise.resolve();\n\n const badge = tint.badge != null ? normalizeBadge(tint.badge) : undefined;\n const size = options.size ?? 64;\n const cover = badge?.shape === 'cover';\n const alpha = badge?.opacity ?? 1;\n\n // An opaque `cover` replaces the icon entirely — no base image needed, so draw\n // it synchronously. A translucent cover falls through to composite over the base.\n if (cover && alpha >= 1) {\n const canvas = document.createElement('canvas');\n canvas.width = size;\n canvas.height = size;\n const ctx = canvas.getContext('2d');\n if (ctx) {\n drawBadge(ctx, size, badge);\n applyFavicon(canvas.toDataURL('image/png'), 'image/png');\n }\n return Promise.resolve();\n }\n\n // Plain image swap: a custom `src` with nothing to composite skips the canvas.\n const needsCanvas = tint.hue != null || Boolean(tint.filter) || Boolean(badge);\n if (!needsCanvas) {\n if (tint.src) applyFavicon(tint.src, inferType(tint.src));\n return Promise.resolve();\n }\n\n originalSource ??= currentIconHref();\n const source = tint.src ?? options.source ?? originalSource ?? '/favicon.ico';\n\n return new Promise<void>((resolve) => {\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.decoding = 'async';\n img.addEventListener('error', () => resolve());\n img.addEventListener('load', () => {\n try {\n const canvas = document.createElement('canvas');\n canvas.width = size;\n canvas.height = size;\n const ctx = canvas.getContext('2d');\n if (ctx) {\n // `cover` ignores the hue/filter (it's replacing the icon); a plain tint\n // applies it to the base. The base is always drawn here — a translucent\n // cover shows it through.\n if (!cover) {\n const filter = cssFilter(tint);\n if (filter) ctx.filter = filter;\n }\n ctx.drawImage(img, 0, 0, size, size);\n ctx.filter = 'none';\n if (badge) {\n ctx.globalAlpha = alpha;\n drawBadge(ctx, size, badge);\n ctx.globalAlpha = 1;\n }\n applyFavicon(canvas.toDataURL('image/png'), 'image/png');\n }\n } catch {\n // Tainted canvas (cross-origin source without CORS headers) or an\n // unsupported API — leave the existing favicon in place.\n }\n resolve();\n });\n img.src = source;\n });\n}\n"],"mappings":"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","names":["global_exports","__export","envFavicon","DEFAULT_BADGE_COLOR","normalizeBadge","badge","badgeText","placeBadge","corner","cw","ch","bw","bh","margin","parseRgb","color","hex","h","c","n","rgb","contrastColor","defaultDetect","hostname","h","cssFilter","tint","hashHue","input","offset","h","i","ICON_LINK","originalSource","currentIconHref","links","interpolate","template","match","_full","index","name","resolveRuleBadge","badge","url","text","rest","resolved","ruleToTint","rule","tint","matchRules","rules","m","resolveTint","options","matched","offset","hashHue","env","defaultDetect","inferType","src","applyFavicon","href","type","link","contrastText","ctx","background","contrastColor","traceRoundRect","x","y","w","h","r","rr","drawBadge","size","color","DEFAULT_BADGE_COLOR","badgeText","family","maxWidth","fontSize","textWidth","corner","margin","pad","px","py","placeBadge","radius","envFavicon","normalizeBadge","cover","alpha","canvas","source","resolve","img","filter","cssFilter","boot","el","data","envFavicon","environments","name","value","hue"]}