favicon-env 0.1.0 → 0.2.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 CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
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
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)
5
+ ![favicon-env — the same base icon per environment: prod, dev (hue-shift), dev (exact-colour tint), staging (dot), a "#344" preview cover, and a custom image](docs/hero.png)
6
6
 
7
7
  **[▶ Live, clickable demo](https://amir-abushanab.github.io/favicon-env/)**
8
8
 
9
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
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).
11
+ - **Zero dependencies.** ~2.3 kB min+gzip (the build-time SSR helper alone is ~1.7 kB).
12
12
 
13
13
  ## Install
14
14
 
@@ -127,6 +127,23 @@ envFavicon({
127
127
 
128
128
  `detect` can key off anything, not just the hostname — e.g. `detect: () => import.meta.env.MODE`.
129
129
 
130
+ ### Exact colours
131
+
132
+ `hue` *rotates* every colour by a fixed angle — a relative shift that keeps the icon's shading **and** saturation, so it can't target a specific colour (and it barely moves white/black/grey). For a **specific** colour, use `tint`: it recolours the icon to that exact colour as a duotone — shape and shading kept, the original colours replaced — so a white logo becomes solid `tint`:
133
+
134
+ ```js
135
+ envFavicon({
136
+ environments: {
137
+ dev: { tint: '#22c55e' }, // exact green (shape + shading kept)
138
+ staging: { tint: '#f59e0b' }, // exact amber
139
+ },
140
+ })
141
+ ```
142
+
143
+ Want a flat single-colour block instead of a duotone? Use a text-less `cover` badge (`{ badge: { color: '#22c55e', shape: 'cover' } }`). Want a different icon entirely? Use `src`.
144
+
145
+ Any colour (`tint`, `badge.color`, `textColor`) can be **any CSS colour** — including CSS Color 4 spaces like `oklch()`, `oklab()`, `lab()`, `lch()`, and `color(display-p3 …)` on browsers that support them (they degrade gracefully elsewhere). Badge text auto-contrasts against `oklch`/`oklab`/`lab`/`lch` colours too.
146
+
130
147
  ### Auto mode
131
148
 
132
149
  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:
@@ -284,7 +301,7 @@ Now `vite dev` and `vite build` serve the tint baked into the initial HTML — n
284
301
  | `source` | `string` | current icon / `.ico`| Favicon URL to tint. |
285
302
  | `size` | `number` | `64` | Canvas raster size in px. |
286
303
 
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`.
304
+ `EnvTint`: `{ hue?: number; tint?: string; filter?: string; src?: string; badge?: string | Badge }`. `hue` rotates the icon's hue (relative); `tint` colourises it to an *exact* colour (a duotone that keeps the artwork's shape + shading — a white logo becomes solid `tint`); `filter` (any CSS filter) beats both; `src` replaces the base image for that env; `badge` is a colour string (a dot) or a `Badge`. Everything composites in one pass.
288
305
 
289
306
  `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
307
 
@@ -26,11 +26,21 @@ function parseRgb(color) {
26
26
  if (rgb) return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])];
27
27
  return null;
28
28
  }
29
- function contrastColor(color) {
29
+ function lightness(color) {
30
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";
31
+ if (rgb) return (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;
32
+ const lch = /^(okl(?:ch|ab)|l(?:ch|ab))\(\s*([\d.]+)(%?)/i.exec(color);
33
+ if (lch) {
34
+ const l = Number(lch[2]);
35
+ if (lch[3]) return l / 100;
36
+ return /^ok/i.test(lch[1]) ? l : l / 100;
37
+ }
38
+ return null;
39
+ }
40
+ function contrastColor(color) {
41
+ const l = lightness(color);
42
+ if (l == null) return "#fff";
43
+ return l > 0.6 ? "#000" : "#fff";
34
44
  }
35
45
 
36
46
  // src/filter.ts
@@ -111,11 +121,15 @@ function svgCover([minX, minY, w, h], badge) {
111
121
  );
112
122
  return `<g${op}>${rect}${label}</g>`;
113
123
  }
124
+ function svgColorize(color) {
125
+ return `<filter id="__favenv_c" color-interpolation-filters="sRGB"><feColorMatrix type="saturate" values="0" result="g"/><feFlood flood-color="${escapeXml(color)}" result="f"/><feBlend in="f" in2="g" mode="multiply" result="b"/><feComposite in="b" in2="SourceGraphic" operator="in"/></filter>`;
126
+ }
114
127
  function tintSvg(svg, tint) {
115
128
  if (!tint) return svg;
116
129
  const filter = cssFilter(tint);
130
+ const colorize = tint.filter ? void 0 : tint.tint;
117
131
  const badge = tint.badge ? normalizeBadge(tint.badge) : null;
118
- if (!filter && !badge) return svg;
132
+ if (!filter && !colorize && !badge) return svg;
119
133
  const open = /<svg\b[^>]*>/i.exec(svg);
120
134
  if (!open) return svg;
121
135
  const close = svg.lastIndexOf("</svg>");
@@ -128,11 +142,18 @@ function tintSvg(svg, tint) {
128
142
  return `${svg.slice(0, openEnd)}${base}${svgCover(vb, badge)}${svg.slice(close)}`;
129
143
  }
130
144
  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;
145
+ let defs = "";
146
+ let body = inner;
147
+ if (colorize) {
148
+ defs = svgColorize(colorize);
149
+ body = `<g filter="url(#__favenv_c)">${inner}</g>`;
150
+ } else if (filter) {
151
+ defs = `<style>.__favenv{filter:${filter}}</style>`;
152
+ body = `<g class="__favenv">${inner}</g>`;
153
+ }
133
154
  const viewBox = badge ? parseViewBox(open[0]) : null;
134
155
  const badgeSvg = badge && viewBox ? svgBadge(viewBox, badge) : "";
135
- return `${svg.slice(0, openEnd)}${style}${body}${badgeSvg}${svg.slice(close)}`;
156
+ return `${svg.slice(0, openEnd)}${defs}${body}${badgeSvg}${svg.slice(close)}`;
136
157
  }
137
158
  function svgToDataUri(svg) {
138
159
  return `data:image/svg+xml,${encodeURIComponent(svg)}`;
@@ -142,5 +163,5 @@ function faviconDataUri(svg, tint) {
142
163
  }
143
164
 
144
165
  export { DEFAULT_BADGE_COLOR, badgeText, contrastColor, cssFilter, faviconDataUri, normalizeBadge, placeBadge, svgToDataUri, tintSvg };
145
- //# sourceMappingURL=chunk-NRMMGQRU.js.map
146
- //# sourceMappingURL=chunk-NRMMGQRU.js.map
166
+ //# sourceMappingURL=chunk-3JXNCGZF.js.map
167
+ //# sourceMappingURL=chunk-3JXNCGZF.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-3JXNCGZF.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 * Perceived lightness (0–1) of `color` for the black/white text decision:\n * rec601 luma for `#hex` / `rgb()`, or the `L` channel of CSS Color 4 colours —\n * `oklch()`/`oklab()` (0–1) and `lab()`/`lch()` (0–100). `null` if unreadable.\n */\nfunction lightness(color: string): number | null {\n const rgb = parseRgb(color);\n if (rgb) return (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;\n const lch = /^(okl(?:ch|ab)|l(?:ch|ab))\\(\\s*([\\d.]+)(%?)/i.exec(color);\n if (lch) {\n const l = Number(lch[2]);\n if (lch[3]) return l / 100; // a percentage L, in any of these spaces\n return /^ok/i.test(lch[1]) ? l : l / 100; // oklab/oklch L is 0–1, lab/lch is 0–100\n }\n return null;\n}\n\n/**\n * Black or white — whichever reads better on `color` — by perceived lightness.\n * Handles `#hex`, `rgb()`, and CSS Color 4 `oklch()`/`oklab()`/`lab()`/`lch()`;\n * falls back to white for anything else (named colours, `hsl()`, `color()`, …).\n */\nexport function contrastColor(color: string): string {\n const l = lightness(color);\n if (l == null) return '#fff';\n return l > 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 * An SVG `<filter>` that colourises the icon to `color`, mirroring the runtime\n * canvas path: desaturate → multiply the flood colour → re-mask the original\n * alpha. `color-interpolation-filters=\"sRGB\"` keeps it visually matching canvas.\n */\nfunction svgColorize(color: string): string {\n return (\n `<filter id=\"__favenv_c\" color-interpolation-filters=\"sRGB\">` +\n `<feColorMatrix type=\"saturate\" values=\"0\" result=\"g\"/>` +\n `<feFlood flood-color=\"${escapeXml(color)}\" result=\"f\"/>` +\n `<feBlend in=\"f\" in2=\"g\" mode=\"multiply\" result=\"b\"/>` +\n `<feComposite in=\"b\" in2=\"SourceGraphic\" operator=\"in\"/>` +\n `</filter>`\n );\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 colorize = tint.filter ? undefined : tint.tint; // explicit `filter` beats `tint`\n const badge = tint.badge ? normalizeBadge(tint.badge) : null;\n if (!filter && !colorize && !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 // `tint` colourises via an SVG `<filter>`; else `hue`/`filter` is a CSS filter.\n let defs = '';\n let body = inner;\n if (colorize) {\n defs = svgColorize(colorize);\n body = `<g filter=\"url(#__favenv_c)\">${inner}</g>`;\n } else if (filter) {\n defs = `<style>.__favenv{filter:${filter}}</style>`;\n body = `<g class=\"__favenv\">${inner}</g>`;\n }\n const viewBox = badge ? parseViewBox(open[0]) : null;\n const badgeSvg = badge && viewBox ? svgBadge(viewBox, badge) : '';\n return `${svg.slice(0, openEnd)}${defs}${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"]}
@@ -1,2 +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);})();
1
+ "use strict";var faviconEnv=(()=>{var h=Object.defineProperty;var U=Object.getOwnPropertyDescriptor;var W=Object.getOwnPropertyNames;var P=Object.prototype.hasOwnProperty;var H=(e,t)=>{for(var n in t)h(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))!P.call(e,o)&&o!==n&&h(e,o,{get:()=>t[o],enumerable:!(r=U(t,o))||r.enumerable});return e};var j=e=>_(h({},"__esModule",{value:!0}),e);var z={};H(z,{envFavicon:()=>p});var M="#ef4444";function R(e){return typeof e=="string"?{color:e}:e}function C(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 G(e){let t=q(e);if(t)return(.299*t[0]+.587*t[1]+.114*t[2])/255;let n=/^(okl(?:ch|ab)|l(?:ch|ab))\(\s*([\d.]+)(%?)/i.exec(e);if(n){let r=Number(n[2]);return n[3]?r/100:/^ok/i.test(n[1])?r:r/100}return null}function B(e){let t=G(e);return t==null?"#fff":t>.6?"#000":"#fff"}function L(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 S(e){return e.filter?e.filter:typeof e.hue=="number"?`hue-rotate(${e.hue}deg)`:null}function O(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 k='link[rel~="icon"]',$;function K(){let e=document.querySelectorAll(k);return e.item(e.length-1)?.href||void 0}function J(e,t){return e.replace(/\$(?:(\d+)|<([^>]+)>)/g,(n,r,o)=>o==null?t[Number(r)]??"":t.groups?.[o]??"")}function Q(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=J(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=Q(e.badge,t,n)),r}function V(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 X(e){let t=typeof location>"u"?null:new URL(location.href);if(e.rules&&t){let o=V(e.rules,t);if(o)return o}if(e.auto){let o=typeof e.auto=="object"?e.auto.offset??0:0;return{hue:O(t?.host??"",o)}}let n=(e.detect??L)();return(n?e.environments?.[n]:void 0)||null}function Y(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 v(e,t){document.querySelectorAll(k).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 F(e,t){return e.fillStyle=t,B(e.fillStyle)}function N(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??M,o=C(n),i='system-ui, -apple-system, "Segoe UI", sans-serif';if(n.shape==="cover"){if(N(e,0,0,t,t,Math.round(t*.2)),e.fillStyle=r,e.fill(),o){let E=t*.84,g=Math.round(t*.62);e.font=`700 ${g}px ${i}`;let T=e.measureText(o).width;T>E&&(g=Math.max(7,Math.floor(g*(E/T))),e.font=`700 ${g}px ${i}`),e.fillStyle=n.textColor??F(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)),u=Math.round(t*.02),l=t-u*2,d=Math.round(s*.5),a=Math.round(s*.66);e.font=`700 ${a}px ${i}`;let f=o?Math.max(s,Math.ceil(e.measureText(o).width)+d):s;f>l&&(a=Math.max(7,Math.floor(a*(l/f))),e.font=`700 ${a}px ${i}`,f=Math.min(l,Math.max(s,Math.ceil(e.measureText(o).width)+d)));let[m,w]=x(c,t,t,f,s,u),b=Math.round(m),y=Math.round(w),I=o?Math.min(s/2,t*.24):s/2;N(e,b,y,f,s,I),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??F(e,r),e.textAlign="center",e.textBaseline="middle",e.fillText(o,b+f/2,y+s/2))}function p(e={}){if(typeof document>"u"||typeof HTMLCanvasElement>"u")return Promise.resolve();let t=X(e);if(!t)return Promise.resolve();let n=t.badge!=null?R(t.badge):void 0,r=e.size??64,o=n?.shape==="cover",i=n?.opacity??1;if(o&&i>=1){let u=document.createElement("canvas");u.width=r,u.height=r;let l=u.getContext("2d");return l&&(D(l,r,n),v(u.toDataURL("image/png"),"image/png")),Promise.resolve()}if(!(t.hue!=null||!!t.filter||!!t.tint||!!n))return t.src&&v(t.src,Y(t.src)),Promise.resolve();$??($=K());let s=t.src??e.source??$??"/favicon.ico";return new Promise(u=>{let l=new Image;l.crossOrigin="anonymous",l.decoding="async",l.addEventListener("error",()=>u()),l.addEventListener("load",()=>{try{let d=document.createElement("canvas");d.width=r,d.height=r;let a=d.getContext("2d");if(a){let f=!o&&!t.filter?t.tint:void 0;if(f)a.filter="grayscale(1)",a.drawImage(l,0,0,r,r),a.filter="none",a.globalCompositeOperation="multiply",a.fillStyle=f,a.fillRect(0,0,r,r),a.globalCompositeOperation="destination-in",a.drawImage(l,0,0,r,r),a.globalCompositeOperation="source-over";else{if(!o){let m=S(t);m&&(a.filter=m)}a.drawImage(l,0,0,r,r),a.filter="none"}n&&(a.globalAlpha=i,D(a,r,n),a.globalAlpha=1),v(d.toDataURL("image/png"),"image/png")}}catch{}u()}),l.src=s})}function Z(){let e=document.currentScript;if(!e)return;let t=e.dataset;if(t.auto!==void 0){p({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&&p({environments:n})}Z();return j(z);})();
2
2
  //# sourceMappingURL=favicon-env.global.js.map
@@ -1 +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":"8bAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,gBAAAE,ICGO,IAAMC,EAAsB,UAG5B,SAASC,EAAeC,EAA8B,CAC3D,OAAO,OAAOA,GAAU,SAAW,CAAE,MAAOA,CAAM,EAAIA,CACxD,CAGO,SAASC,EAAUD,EAAsB,CAC9C,OAAOA,EAAM,MAAQ,KAAO,GAAK,OAAOA,EAAM,IAAI,CACpD,CAOO,SAASE,EACdC,EACAC,EACAC,EACAC,EACAC,EACAC,EACkB,CAClB,OAAIL,IAAW,SAAiB,EAAEC,EAAKE,GAAM,GAAID,EAAKE,GAAM,CAAC,EACtD,CACLJ,EAAO,SAAS,MAAM,EAAIK,EAASJ,EAAKE,EAAKE,EAC7CL,EAAO,WAAW,KAAK,EAAIK,EAASH,EAAKE,EAAKC,CAChD,CACF,CChCA,SAASC,EAASC,EAAgD,CAChE,IAAMC,EAAM,gCAAgC,KAAKD,CAAK,EACtD,GAAIC,EAAK,CACP,IAAMC,EAAID,EAAI,CAAC,EAAE,SAAW,EAAIA,EAAI,CAAC,EAAE,QAAQ,KAAOE,GAAMA,EAAIA,CAAC,EAAIF,EAAI,CAAC,EACpEG,EAAI,OAAO,SAASF,EAAG,EAAE,EAC/B,MAAO,CAAEE,GAAK,GAAM,IAAMA,GAAK,EAAK,IAAKA,EAAI,GAAG,CAClD,CACA,IAAMC,EAAM,mCAAmC,KAAKL,CAAK,EACzD,OAAIK,EAAY,CAAC,OAAOA,EAAI,CAAC,CAAC,EAAG,OAAOA,EAAI,CAAC,CAAC,EAAG,OAAOA,EAAI,CAAC,CAAC,CAAC,EACxD,IACT,CAMO,SAASC,EAAcN,EAAuB,CACnD,IAAMK,EAAMN,EAASC,CAAK,EAC1B,OAAKK,IACc,KAAQA,EAAI,CAAC,EAAI,KAAQA,EAAI,CAAC,EAAI,KAAQA,EAAI,CAAC,GAAK,IACpD,GAAM,OAFR,MAGnB,CCTO,SAASE,EACdC,EAAmB,OAAO,SAAa,IAAc,GAAK,SAAS,SAC3D,CACR,IAAMC,EAAID,EAAS,YAAY,EAC/B,OACEC,IAAM,aACNA,IAAM,aACNA,IAAM,OACNA,IAAM,SACNA,EAAE,SAAS,QAAQ,GACnBA,EAAE,SAAS,YAAY,GACvB,4BAA4B,KAAKA,CAAC,EAE3B,MAEL,sEAAsE,KAAKA,CAAC,EACvE,UAEF,MACT,CC7BO,SAASC,EAAUC,EAA8B,CACtD,OAAIA,EAAK,OAAeA,EAAK,OACzB,OAAOA,EAAK,KAAQ,SAAiB,cAAcA,EAAK,GAAG,OACxD,IACT,CCCO,SAASC,EAAQC,EAAeC,EAAS,EAAW,CACzD,IAAIC,EAAI,WACR,QAASC,EAAI,EAAGA,EAAIH,EAAM,OAAQG,GAAK,EACrCD,GAAKF,EAAM,WAAWG,CAAC,EACvBD,EAAI,KAAK,KAAKA,EAAG,QAAU,EAE7B,QAAUA,IAAM,GAAK,IAAQD,EAAS,IAAO,KAAO,GACtD,CCPA,IAAMG,EAAY,oBAGdC,EAEJ,SAASC,GAAsC,CAC7C,IAAMC,EAAQ,SAAS,iBAAkCH,CAAS,EAElE,OADaG,EAAM,KAAKA,EAAM,OAAS,CAAC,GAC3B,MAAQ,MACvB,CAGA,SAASC,EAAYC,EAAkBC,EAAiC,CACtE,OAAOD,EAAS,QAAQ,yBAA0B,CAACE,EAAOC,EAAOC,IAC/DA,GAAQ,KAAQH,EAAM,OAAOE,CAAK,CAAC,GAAK,GAAOF,EAAM,SAASG,CAAI,GAAK,EACzE,CACF,CAEA,SAASC,EACPC,EACAL,EACAM,EACO,CACP,GAAI,OAAOD,GAAU,SAAU,MAAO,CAAE,MAAOA,CAAM,EACrD,GAAM,CAAE,KAAAE,EAAM,GAAGC,CAAK,EAAIH,EACtBI,EACJ,OAAI,OAAOF,GAAS,WAAYE,EAAWF,EAAKP,EAAOM,CAAG,EACjD,OAAOC,GAAS,UAAYP,EAAOS,EAAWX,EAAYS,EAAMP,CAAK,EACzES,EAAWF,EACT,CAAE,GAAGC,EAAM,KAAMC,CAAS,CACnC,CAEA,SAASC,EAAWC,EAAeX,EAAgCM,EAAmB,CACpF,IAAMM,EAAgB,CAAC,EACvB,OAAID,EAAK,KAAO,OAAMC,EAAK,IAAMD,EAAK,KAClCA,EAAK,SAAQC,EAAK,OAASD,EAAK,QAChCA,EAAK,MAAKC,EAAK,IAAMD,EAAK,KAC1BA,EAAK,OAAS,OAAMC,EAAK,MAAQR,EAAiBO,EAAK,MAAOX,EAAOM,CAAG,GACrEM,CACT,CASO,SAASC,EAAWC,EAAkBR,EAA0B,CACrE,QAAWK,KAAQG,EACjB,GAAI,OAAOH,EAAK,OAAU,YACxB,GAAIA,EAAK,MAAML,CAAG,EAAG,OAAOI,EAAWC,EAAM,KAAML,CAAG,MACjD,CACL,IAAMS,EAAIT,EAAI,KAAK,MAAMK,EAAK,KAAK,EACnC,GAAII,EAAG,OAAOL,EAAWC,EAAMI,EAAGT,CAAG,CACvC,CAEF,OAAO,IACT,CAEA,SAASU,EAAYC,EAA4C,CAC/D,IAAMX,EAAM,OAAO,SAAa,IAAc,KAAO,IAAI,IAAI,SAAS,IAAI,EAC1E,GAAIW,EAAQ,OAASX,EAAK,CACxB,IAAMY,EAAUL,EAAWI,EAAQ,MAAOX,CAAG,EAC7C,GAAIY,EAAS,OAAOA,CACtB,CACA,GAAID,EAAQ,KAAM,CAChB,IAAME,EAAS,OAAOF,EAAQ,MAAS,SAAYA,EAAQ,KAAK,QAAU,EAAK,EAC/E,MAAO,CAAE,IAAKG,EAAQd,GAAK,MAAQ,GAAIa,CAAM,CAAE,CACjD,CACA,IAAME,GAAOJ,EAAQ,QAAUK,GAAe,EAE9C,OADaD,EAAMJ,EAAQ,eAAeI,CAAG,EAAI,SAClC,IACjB,CAEA,SAASE,EAAUC,EAAiC,CAClD,GAAIA,EAAI,WAAW,OAAO,EAAG,MAAO,iBAAiB,KAAKA,CAAG,IAAI,CAAC,EAClE,GAAI,mBAAmB,KAAKA,CAAG,EAAG,MAAO,gBACzC,GAAI,mBAAmB,KAAKA,CAAG,EAAG,MAAO,YACzC,GAAI,mBAAmB,KAAKA,CAAG,EAAG,MAAO,cAE3C,CAEA,SAASC,EAAaC,EAAcC,EAAqB,CACvD,SAAS,iBAAiBjC,CAAS,EAAE,QAASkC,GAASA,EAAK,OAAO,CAAC,EACpE,IAAMA,EAAO,SAAS,cAAc,MAAM,EAC1CA,EAAK,IAAM,OACPD,IAAMC,EAAK,KAAOD,GACtBC,EAAK,KAAOF,EACZE,EAAK,QAAQ,WAAa,GAC1B,SAAS,KAAK,OAAOA,CAAI,CAC3B,CAEA,SAASC,EAAaC,EAA+BC,EAA4B,CAC/E,OAAAD,EAAI,UAAYC,EACTC,EAAcF,EAAI,SAAS,CACpC,CAEA,SAASG,EACPH,EACAI,EACAC,EACAC,EACAC,EACAC,EACM,CACN,IAAMC,EAAK,KAAK,IAAID,EAAGF,EAAI,EAAGC,EAAI,CAAC,EACnCP,EAAI,UAAU,EACdA,EAAI,OAAOI,EAAIK,EAAIJ,CAAC,EACpBL,EAAI,MAAMI,EAAIE,EAAGD,EAAGD,EAAIE,EAAGD,EAAIE,EAAGE,CAAE,EACpCT,EAAI,MAAMI,EAAIE,EAAGD,EAAIE,EAAGH,EAAGC,EAAIE,EAAGE,CAAE,EACpCT,EAAI,MAAMI,EAAGC,EAAIE,EAAGH,EAAGC,EAAGI,CAAE,EAC5BT,EAAI,MAAMI,EAAGC,EAAGD,EAAIE,EAAGD,EAAGI,CAAE,EAC5BT,EAAI,UAAU,CAChB,CAEA,SAASU,EAAUV,EAA+BW,EAAcpC,EAAoB,CAClF,IAAMqC,EAAQrC,EAAM,OAASsC,EACvBpC,EAAOqC,EAAUvC,CAAK,EACtBwC,EAAS,mDAGf,GAAIxC,EAAM,QAAU,QAAS,CAI3B,GAHA4B,EAAeH,EAAK,EAAG,EAAGW,EAAMA,EAAM,KAAK,MAAMA,EAAO,EAAG,CAAC,EAC5DX,EAAI,UAAYY,EAChBZ,EAAI,KAAK,EACLvB,EAAM,CACR,IAAMuC,EAAWL,EAAO,IACpBM,EAAW,KAAK,MAAMN,EAAO,GAAI,EACrCX,EAAI,KAAO,OAAOiB,CAAQ,MAAMF,CAAM,GACtC,IAAMG,EAAYlB,EAAI,YAAYvB,CAAI,EAAE,MACpCyC,EAAYF,IACdC,EAAW,KAAK,IAAI,EAAG,KAAK,MAAMA,GAAYD,EAAWE,EAAU,CAAC,EACpElB,EAAI,KAAO,OAAOiB,CAAQ,MAAMF,CAAM,IAExCf,EAAI,UAAYzB,EAAM,WAAawB,EAAaC,EAAKY,CAAK,EAC1DZ,EAAI,UAAY,SAChBA,EAAI,aAAe,SACnBA,EAAI,SAASvB,EAAMkC,EAAO,EAAGA,EAAO,CAAC,CACvC,CACA,MACF,CAEA,IAAMQ,EAAS5C,EAAM,QAAU,eACzBgC,EAAI,KAAK,MAAMI,GAAQpC,EAAM,MAAQ,GAAI,EACzC6C,EAAS,KAAK,MAAMT,EAAO,GAAI,EAC/BK,EAAWL,EAAOS,EAAS,EAC3BC,EAAM,KAAK,MAAMd,EAAI,EAAG,EAG1BU,EAAW,KAAK,MAAMV,EAAI,GAAI,EAClCP,EAAI,KAAO,OAAOiB,CAAQ,MAAMF,CAAM,GACtC,IAAIT,EAAI7B,EAAO,KAAK,IAAI8B,EAAG,KAAK,KAAKP,EAAI,YAAYvB,CAAI,EAAE,KAAK,EAAI4C,CAAG,EAAId,EACvED,EAAIU,IACNC,EAAW,KAAK,IAAI,EAAG,KAAK,MAAMA,GAAYD,EAAWV,EAAE,CAAC,EAC5DN,EAAI,KAAO,OAAOiB,CAAQ,MAAMF,CAAM,GACtCT,EAAI,KAAK,IAAIU,EAAU,KAAK,IAAIT,EAAG,KAAK,KAAKP,EAAI,YAAYvB,CAAI,EAAE,KAAK,EAAI4C,CAAG,CAAC,GAElF,GAAM,CAACC,EAAIC,CAAE,EAAIC,EAAWL,EAAQR,EAAMA,EAAML,EAAGC,EAAGa,CAAM,EACtDhB,EAAI,KAAK,MAAMkB,CAAE,EACjB,EAAI,KAAK,MAAMC,CAAE,EACjBE,EAAShD,EAAO,KAAK,IAAI8B,EAAI,EAAGI,EAAO,GAAI,EAAIJ,EAAI,EAEzDJ,EAAeH,EAAKI,EAAG,EAAGE,EAAGC,EAAGkB,CAAM,EACtCzB,EAAI,UAAYY,EAChBZ,EAAI,KAAK,EACTA,EAAI,UAAY,KAAK,IAAI,EAAGW,EAAO,GAAI,EACvCX,EAAI,YAAc,sBAClBA,EAAI,OAAO,EAEPvB,IACFuB,EAAI,UAAYzB,EAAM,WAAawB,EAAaC,EAAKY,CAAK,EAC1DZ,EAAI,UAAY,SAChBA,EAAI,aAAe,SACnBA,EAAI,SAASvB,EAAM2B,EAAIE,EAAI,EAAG,EAAIC,EAAI,CAAC,EAE3C,CAiBO,SAASmB,EAAWvC,EAA6B,CAAC,EAAkB,CACzE,GAAI,OAAO,SAAa,KAAe,OAAO,kBAAsB,IAClE,OAAO,QAAQ,QAAQ,EAEzB,IAAML,EAAOI,EAAYC,CAAO,EAChC,GAAI,CAACL,EAAM,OAAO,QAAQ,QAAQ,EAElC,IAAMP,EAAQO,EAAK,OAAS,KAAO6C,EAAe7C,EAAK,KAAK,EAAI,OAC1D6B,EAAOxB,EAAQ,MAAQ,GACvByC,EAAQrD,GAAO,QAAU,QACzBsD,EAAQtD,GAAO,SAAW,EAIhC,GAAIqD,GAASC,GAAS,EAAG,CACvB,IAAMC,EAAS,SAAS,cAAc,QAAQ,EAC9CA,EAAO,MAAQnB,EACfmB,EAAO,OAASnB,EAChB,IAAMX,EAAM8B,EAAO,WAAW,IAAI,EAClC,OAAI9B,IACFU,EAAUV,EAAKW,EAAMpC,CAAK,EAC1BoB,EAAamC,EAAO,UAAU,WAAW,EAAG,WAAW,GAElD,QAAQ,QAAQ,CACzB,CAIA,GAAI,EADgBhD,EAAK,KAAO,MAAQ,EAAQA,EAAK,QAAW,EAAQP,GAEtE,OAAIO,EAAK,KAAKa,EAAab,EAAK,IAAKW,EAAUX,EAAK,GAAG,CAAC,EACjD,QAAQ,QAAQ,EAGzBjB,MAAmBC,EAAgB,GACnC,IAAMiE,EAASjD,EAAK,KAAOK,EAAQ,QAAUtB,GAAkB,eAE/D,OAAO,IAAI,QAAemE,GAAY,CACpC,IAAMC,EAAM,IAAI,MAChBA,EAAI,YAAc,YAClBA,EAAI,SAAW,QACfA,EAAI,iBAAiB,QAAS,IAAMD,EAAQ,CAAC,EAC7CC,EAAI,iBAAiB,OAAQ,IAAM,CACjC,GAAI,CACF,IAAMH,EAAS,SAAS,cAAc,QAAQ,EAC9CA,EAAO,MAAQnB,EACfmB,EAAO,OAASnB,EAChB,IAAMX,EAAM8B,EAAO,WAAW,IAAI,EAClC,GAAI9B,EAAK,CAIP,GAAI,CAAC4B,EAAO,CACV,IAAMM,EAASC,EAAUrD,CAAI,EACzBoD,IAAQlC,EAAI,OAASkC,EAC3B,CACAlC,EAAI,UAAUiC,EAAK,EAAG,EAAGtB,EAAMA,CAAI,EACnCX,EAAI,OAAS,OACTzB,IACFyB,EAAI,YAAc6B,EAClBnB,EAAUV,EAAKW,EAAMpC,CAAK,EAC1ByB,EAAI,YAAc,GAEpBL,EAAamC,EAAO,UAAU,WAAW,EAAG,WAAW,CACzD,CACF,MAAQ,CAGR,CACAE,EAAQ,CACV,CAAC,EACDC,EAAI,IAAMF,CACZ,CAAC,CACH,CN1QA,SAASK,GAAa,CACpB,IAAMC,EAAK,SAAS,cACpB,GAAI,CAACA,EAAI,OACT,IAAMC,EAAOD,EAAG,QAChB,GAAIC,EAAK,OAAS,OAAW,CACtBC,EAAW,CAAE,KAAM,EAAK,CAAC,EAC9B,MACF,CACA,IAAMC,EAAwC,CAAC,EAC/C,OAAW,CAACC,EAAMC,CAAK,IAAK,OAAO,QAAQJ,CAAI,EAAG,CAChD,IAAMK,EAAM,OAAOD,CAAK,EACpBA,IAAU,QAAaA,IAAU,IAAM,CAAC,OAAO,MAAMC,CAAG,IAC1DH,EAAaC,CAAI,EAAI,CAAE,IAAAE,CAAI,EAE/B,CACI,OAAO,KAAKH,CAAY,EAAE,OAAS,GAAQD,EAAW,CAAE,aAAAC,CAAa,CAAC,CAC5E,CAEAJ,EAAK","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"]}
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 * Perceived lightness (0–1) of `color` for the black/white text decision:\n * rec601 luma for `#hex` / `rgb()`, or the `L` channel of CSS Color 4 colours —\n * `oklch()`/`oklab()` (0–1) and `lab()`/`lch()` (0–100). `null` if unreadable.\n */\nfunction lightness(color: string): number | null {\n const rgb = parseRgb(color);\n if (rgb) return (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;\n const lch = /^(okl(?:ch|ab)|l(?:ch|ab))\\(\\s*([\\d.]+)(%?)/i.exec(color);\n if (lch) {\n const l = Number(lch[2]);\n if (lch[3]) return l / 100; // a percentage L, in any of these spaces\n return /^ok/i.test(lch[1]) ? l : l / 100; // oklab/oklch L is 0–1, lab/lch is 0–100\n }\n return null;\n}\n\n/**\n * Black or white — whichever reads better on `color` — by perceived lightness.\n * Handles `#hex`, `rgb()`, and CSS Color 4 `oklch()`/`oklab()`/`lab()`/`lch()`;\n * falls back to white for anything else (named colours, `hsl()`, `color()`, …).\n */\nexport function contrastColor(color: string): string {\n const l = lightness(color);\n if (l == null) return '#fff';\n return l > 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 =\n tint.hue != null || Boolean(tint.filter) || Boolean(tint.tint) || 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` replaces the icon, so it ignores any recolour. Otherwise:\n // `tint` colourises the base to an exact colour (grayscale → multiply →\n // re-mask the original alpha), else a `hue`/`filter` is a plain CSS\n // filter. The base is always drawn — a translucent cover shows through.\n const colorize = !cover && !tint.filter ? tint.tint : undefined;\n if (colorize) {\n ctx.filter = 'grayscale(1)';\n ctx.drawImage(img, 0, 0, size, size);\n ctx.filter = 'none';\n ctx.globalCompositeOperation = 'multiply';\n ctx.fillStyle = colorize;\n ctx.fillRect(0, 0, size, size);\n ctx.globalCompositeOperation = 'destination-in';\n ctx.drawImage(img, 0, 0, size, size);\n ctx.globalCompositeOperation = 'source-over';\n } else {\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 }\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","lightness","lch","l","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","colorize","filter","cssFilter","boot","el","data","envFavicon","environments","name","value","hue"]}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { E as EnvFaviconOptions, a as EnvRule, b as EnvTint } from './ssr-CI5GvAY9.js';
2
- export { A as AutoOptions, B as Badge, c as BadgeCorner, d as BadgeTextFn, e as EnvConfig, R as RuleBadge, f as faviconDataUri, s as svgToDataUri, t as tintSvg } from './ssr-CI5GvAY9.js';
1
+ import { E as EnvFaviconOptions, a as EnvRule, b as EnvTint } from './ssr-BzQ5WSMw.js';
2
+ export { A as AutoOptions, B as Badge, c as BadgeCorner, d as BadgeTextFn, e as EnvConfig, R as RuleBadge, f as faviconDataUri, s as svgToDataUri, t as tintSvg } from './ssr-BzQ5WSMw.js';
3
3
 
4
4
  /**
5
5
  * Return the tint of the first `rule` whose `match` matches `url`, or `null` if
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { normalizeBadge, cssFilter, DEFAULT_BADGE_COLOR, badgeText, placeBadge, contrastColor } from './chunk-NRMMGQRU.js';
2
- export { faviconDataUri, svgToDataUri, tintSvg } from './chunk-NRMMGQRU.js';
1
+ import { normalizeBadge, cssFilter, DEFAULT_BADGE_COLOR, badgeText, placeBadge, contrastColor } from './chunk-3JXNCGZF.js';
2
+ export { faviconDataUri, svgToDataUri, tintSvg } from './chunk-3JXNCGZF.js';
3
3
 
4
4
  // src/detect.ts
5
5
  function defaultDetect(hostname = typeof location === "undefined" ? "" : location.hostname) {
@@ -184,7 +184,7 @@ function envFavicon(options = {}) {
184
184
  }
185
185
  return Promise.resolve();
186
186
  }
187
- const needsCanvas = tint.hue != null || Boolean(tint.filter) || Boolean(badge);
187
+ const needsCanvas = tint.hue != null || Boolean(tint.filter) || Boolean(tint.tint) || Boolean(badge);
188
188
  if (!needsCanvas) {
189
189
  if (tint.src) applyFavicon(tint.src, inferType(tint.src));
190
190
  return Promise.resolve();
@@ -203,12 +203,25 @@ function envFavicon(options = {}) {
203
203
  canvas.height = size;
204
204
  const ctx = canvas.getContext("2d");
205
205
  if (ctx) {
206
- if (!cover) {
207
- const filter = cssFilter(tint);
208
- if (filter) ctx.filter = filter;
206
+ const colorize = !cover && !tint.filter ? tint.tint : void 0;
207
+ if (colorize) {
208
+ ctx.filter = "grayscale(1)";
209
+ ctx.drawImage(img, 0, 0, size, size);
210
+ ctx.filter = "none";
211
+ ctx.globalCompositeOperation = "multiply";
212
+ ctx.fillStyle = colorize;
213
+ ctx.fillRect(0, 0, size, size);
214
+ ctx.globalCompositeOperation = "destination-in";
215
+ ctx.drawImage(img, 0, 0, size, size);
216
+ ctx.globalCompositeOperation = "source-over";
217
+ } else {
218
+ if (!cover) {
219
+ const filter = cssFilter(tint);
220
+ if (filter) ctx.filter = filter;
221
+ }
222
+ ctx.drawImage(img, 0, 0, size, size);
223
+ ctx.filter = "none";
209
224
  }
210
- ctx.drawImage(img, 0, 0, size, size);
211
- ctx.filter = "none";
212
225
  if (badge) {
213
226
  ctx.globalAlpha = alpha;
214
227
  drawBadge(ctx, size, badge);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/detect.ts","../src/hash.ts","../src/tint.ts"],"names":["link","maxWidth","fontSize"],"mappings":"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","file":"index.js","sourcesContent":["/**\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","/**\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"]}
1
+ {"version":3,"sources":["../src/detect.ts","../src/hash.ts","../src/tint.ts"],"names":["link","maxWidth","fontSize"],"mappings":"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","file":"index.js","sourcesContent":["/**\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","/**\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 =\n tint.hue != null || Boolean(tint.filter) || Boolean(tint.tint) || 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` replaces the icon, so it ignores any recolour. Otherwise:\n // `tint` colourises the base to an exact colour (grayscale → multiply →\n // re-mask the original alpha), else a `hue`/`filter` is a plain CSS\n // filter. The base is always drawn — a translucent cover shows through.\n const colorize = !cover && !tint.filter ? tint.tint : undefined;\n if (colorize) {\n ctx.filter = 'grayscale(1)';\n ctx.drawImage(img, 0, 0, size, size);\n ctx.filter = 'none';\n ctx.globalCompositeOperation = 'multiply';\n ctx.fillStyle = colorize;\n ctx.fillRect(0, 0, size, size);\n ctx.globalCompositeOperation = 'destination-in';\n ctx.drawImage(img, 0, 0, size, size);\n ctx.globalCompositeOperation = 'source-over';\n } else {\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 }\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"]}
@@ -2,9 +2,16 @@
2
2
  interface EnvTint {
3
3
  /** Hue rotation in degrees applied to the whole icon (e.g. `120`). */
4
4
  hue?: number;
5
+ /**
6
+ * Colourise the icon to this exact colour (any CSS colour), preserving the
7
+ * artwork's shape and relative shading — a duotone from black up to `tint`.
8
+ * Unlike `hue` (a *relative* rotation) this sets an *absolute* colour, so a
9
+ * white logo becomes solid `tint`. Beats `hue`; an explicit `filter` beats it.
10
+ */
11
+ tint?: string;
5
12
  /**
6
13
  * Explicit CSS filter string, e.g. `'hue-rotate(120deg) saturate(1.4)'`.
7
- * Takes precedence over `hue` when both are set.
14
+ * Takes precedence over `hue` / `tint` when set.
8
15
  */
9
16
  filter?: string;
10
17
  /**
package/dist/ssr.d.ts CHANGED
@@ -1 +1 @@
1
- export { f as faviconDataUri, s as svgToDataUri, t as tintSvg } from './ssr-CI5GvAY9.js';
1
+ export { f as faviconDataUri, s as svgToDataUri, t as tintSvg } from './ssr-BzQ5WSMw.js';
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-3JXNCGZF.js';
2
2
  //# sourceMappingURL=ssr.js.map
3
3
  //# sourceMappingURL=ssr.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "favicon-env",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "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
6
  "license": "MIT",
@@ -20,10 +20,10 @@
20
20
  ],
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "git+https://github.com/amir-abushanab/favicon-env.git"
23
+ "url": "git+https://github.com/Amir-Abushanab/favicon-env.git"
24
24
  },
25
- "homepage": "https://github.com/amir-abushanab/favicon-env#readme",
26
- "bugs": "https://github.com/amir-abushanab/favicon-env/issues",
25
+ "homepage": "https://github.com/Amir-Abushanab/favicon-env#readme",
26
+ "bugs": "https://github.com/Amir-Abushanab/favicon-env/issues",
27
27
  "sideEffects": false,
28
28
  "exports": {
29
29
  ".": {
@@ -157,6 +157,27 @@ client-entry module (Vite/TanStack `src/main.ts`, or an Astro `<script>`).
157
157
 
158
158
  Source: src/tint.ts (`typeof document` guard), README "Runtime mode"
159
159
 
160
+ ### MEDIUM — Using `hue` when you want an exact colour
161
+
162
+ Wrong:
163
+
164
+ ```js
165
+ // trying to make dev green — but hue-rotate is relative to the base icon
166
+ void envFavicon({ environments: { dev: { hue: 130 } } })
167
+ ```
168
+
169
+ Correct:
170
+
171
+ ```js
172
+ void envFavicon({ environments: { dev: { tint: '#22c55e' } } })
173
+ ```
174
+
175
+ `hue` *rotates* the existing colours (the result depends on the base icon, and it
176
+ barely moves white/black/grey). `tint` colourises to an *exact* colour while
177
+ preserving shape and shading. For a flat block, use a text-less `cover` badge.
178
+
179
+ Source: src/types.ts (EnvTint.tint), src/tint.ts (colorize path)
180
+
160
181
  ### MEDIUM — Multi-digit badge as a corner pill
161
182
 
162
183
  Wrong:
@@ -1 +0,0 @@
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"]}