@pyreon/zero 0.20.0 → 0.21.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
@@ -18,7 +18,7 @@ bun add @pyreon/zero
18
18
  - **Components** — `<Image>` (lazy load, srcset, blur-up), `<Link>` (prefetch, active state), `<Script>` (loading strategies)
19
19
  - **Theme** — Dark/light/system with `theme` signal, `<ThemeToggle>`, and anti-flash inline script
20
20
  - **Fonts** — Google Fonts self-hosting at build time, local fonts, size-adjusted fallbacks
21
- - **Image optimization** — Build-time processing via `?optimize` imports (WebP/AVIF, blur placeholders)
21
+ - **Image optimization** — Build-time processing via `?optimize` imports (WebP/AVIF, blur placeholders). Type the custom queries with one line — `/// <reference types="@pyreon/zero/image-types" />` — which ships ambient `declare module "*?optimize"` / `"*?component"` / `"*?raw"` reusing the plugin's own `ProcessedImage`.
22
22
  - **SEO** — Sitemap, robots.txt, JSON-LD helpers (Vite plugin + dev middleware)
23
23
  - **Middleware** — `cacheMiddleware()`, `securityHeaders()`, `corsMiddleware()`, `rateLimitMiddleware()`, `compressionMiddleware()`
24
24
  - **Adapters** — Node.js, Bun, static, Vercel, Cloudflare Pages, Netlify Functions
package/lib/favicon.js CHANGED
@@ -121,6 +121,20 @@ function faviconPlugin(config) {
121
121
  const svgUrl = localeSource ? localeSource.url : url;
122
122
  const svgPath = localeSource ? localeSource.sourcePath : sourcePath;
123
123
  const isSvgSource = localeSource ? localeSource.source.endsWith(".svg") : config.source.endsWith(".svg");
124
+ if (isSvgSource && (svgUrl.endsWith("/favicon-light.svg") || svgUrl.endsWith("/favicon-dark.svg"))) {
125
+ const isDarkVariant = svgUrl.endsWith("/favicon-dark.svg");
126
+ const variantPath = isDarkVariant ? darkPath ?? svgPath : svgPath;
127
+ try {
128
+ let content = await readFile(variantPath, "utf-8");
129
+ if (!isDarkVariant) {
130
+ if (autoDevBadge) content = addDevBadgeToSvg(content);
131
+ else if (devSourcePath && existsSync(devSourcePath)) content = await readFile(devSourcePath, "utf-8");
132
+ }
133
+ res.setHeader("Content-Type", "image/svg+xml");
134
+ res.end(content);
135
+ return;
136
+ } catch {}
137
+ }
124
138
  if (svgUrl.endsWith("/favicon.svg") && isSvgSource) try {
125
139
  let content = await readFile(svgPath, "utf-8");
126
140
  if (autoDevBadge) content = addDevBadgeToSvg(content);
@@ -197,7 +211,27 @@ function faviconPlugin(config) {
197
211
  const isSvg = config.source.endsWith(".svg");
198
212
  const hasDark = !!config.darkSource;
199
213
  const tags = [];
200
- if (isSvg) tags.push({
214
+ if (isSvg && hasDark) tags.push({
215
+ tag: "link",
216
+ attrs: {
217
+ rel: "icon",
218
+ type: "image/svg+xml",
219
+ href: "/favicon-light.svg",
220
+ "data-favicon-theme": "light"
221
+ },
222
+ injectTo: "head"
223
+ }, {
224
+ tag: "link",
225
+ attrs: {
226
+ rel: "icon",
227
+ type: "image/svg+xml",
228
+ href: "/favicon-dark.svg",
229
+ "data-favicon-theme": "dark",
230
+ media: "not all"
231
+ },
232
+ injectTo: "head"
233
+ });
234
+ else if (isSvg) tags.push({
201
235
  tag: "link",
202
236
  attrs: {
203
237
  rel: "icon",
@@ -389,7 +423,20 @@ async function generateFaviconSet(rootDir, source, darkSource, prefix, config, t
389
423
  let finalSvg = svgContent;
390
424
  if (darkSource) {
391
425
  const darkPath = join(rootDir, darkSource);
392
- if (existsSync(darkPath)) finalSvg = wrapSvgWithDarkMode(svgContent, await readFile(darkPath, "utf-8"));
426
+ if (existsSync(darkPath)) {
427
+ const darkSvg = await readFile(darkPath, "utf-8");
428
+ finalSvg = wrapSvgWithDarkMode(svgContent, darkSvg);
429
+ this.emitFile({
430
+ type: "asset",
431
+ fileName: `${prefix}favicon-light.svg`,
432
+ source: svgContent
433
+ });
434
+ this.emitFile({
435
+ type: "asset",
436
+ fileName: `${prefix}favicon-dark.svg`,
437
+ source: darkSvg
438
+ });
439
+ }
393
440
  }
394
441
  this.emitFile({
395
442
  type: "asset",
@@ -479,8 +526,21 @@ function faviconLinks(locale, config) {
479
526
  const hasLocaleOverride = locale && config.locales?.[locale];
480
527
  const prefix = hasLocaleOverride ? `/${locale}` : "";
481
528
  const isSvg = (hasLocaleOverride ? config.locales[locale].source : config.source).endsWith(".svg");
529
+ const hasDark = !!config.darkSource;
482
530
  const links = [];
483
- if (isSvg) links.push({
531
+ if (isSvg && hasDark) links.push({
532
+ rel: "icon",
533
+ type: "image/svg+xml",
534
+ href: `${prefix}/favicon-light.svg`,
535
+ "data-favicon-theme": "light"
536
+ }, {
537
+ rel: "icon",
538
+ type: "image/svg+xml",
539
+ href: `${prefix}/favicon-dark.svg`,
540
+ "data-favicon-theme": "dark",
541
+ media: "not all"
542
+ });
543
+ else if (isSvg) links.push({
484
544
  rel: "icon",
485
545
  type: "image/svg+xml",
486
546
  href: `${prefix}/favicon.svg`
@@ -170,7 +170,7 @@ export default function SvgComponent(props) {
170
170
  }
171
171
  async function loadDevImage(absPath, rawPath, strategy, placeholderSize) {
172
172
  const metadata = await getImageMetadata(absPath);
173
- const publicPath = rawPath.startsWith("/") ? rawPath : `/@fs/${absPath}`;
173
+ const publicPath = rawPath.startsWith("/") && !existsSync(rawPath) ? rawPath : `/@fs/${absPath}`;
174
174
  return {
175
175
  src: publicPath,
176
176
  srcset: "",
File without changes
package/lib/server.js CHANGED
@@ -2866,6 +2866,20 @@ function faviconPlugin(config) {
2866
2866
  const svgUrl = localeSource ? localeSource.url : url;
2867
2867
  const svgPath = localeSource ? localeSource.sourcePath : sourcePath;
2868
2868
  const isSvgSource = localeSource ? localeSource.source.endsWith(".svg") : config.source.endsWith(".svg");
2869
+ if (isSvgSource && (svgUrl.endsWith("/favicon-light.svg") || svgUrl.endsWith("/favicon-dark.svg"))) {
2870
+ const isDarkVariant = svgUrl.endsWith("/favicon-dark.svg");
2871
+ const variantPath = isDarkVariant ? darkPath ?? svgPath : svgPath;
2872
+ try {
2873
+ let content = await readFile(variantPath, "utf-8");
2874
+ if (!isDarkVariant) {
2875
+ if (autoDevBadge) content = addDevBadgeToSvg(content);
2876
+ else if (devSourcePath && existsSync(devSourcePath)) content = await readFile(devSourcePath, "utf-8");
2877
+ }
2878
+ res.setHeader("Content-Type", "image/svg+xml");
2879
+ res.end(content);
2880
+ return;
2881
+ } catch {}
2882
+ }
2869
2883
  if (svgUrl.endsWith("/favicon.svg") && isSvgSource) try {
2870
2884
  let content = await readFile(svgPath, "utf-8");
2871
2885
  if (autoDevBadge) content = addDevBadgeToSvg(content);
@@ -2942,7 +2956,27 @@ function faviconPlugin(config) {
2942
2956
  const isSvg = config.source.endsWith(".svg");
2943
2957
  const hasDark = !!config.darkSource;
2944
2958
  const tags = [];
2945
- if (isSvg) tags.push({
2959
+ if (isSvg && hasDark) tags.push({
2960
+ tag: "link",
2961
+ attrs: {
2962
+ rel: "icon",
2963
+ type: "image/svg+xml",
2964
+ href: "/favicon-light.svg",
2965
+ "data-favicon-theme": "light"
2966
+ },
2967
+ injectTo: "head"
2968
+ }, {
2969
+ tag: "link",
2970
+ attrs: {
2971
+ rel: "icon",
2972
+ type: "image/svg+xml",
2973
+ href: "/favicon-dark.svg",
2974
+ "data-favicon-theme": "dark",
2975
+ media: "not all"
2976
+ },
2977
+ injectTo: "head"
2978
+ });
2979
+ else if (isSvg) tags.push({
2946
2980
  tag: "link",
2947
2981
  attrs: {
2948
2982
  rel: "icon",
@@ -3134,7 +3168,20 @@ async function generateFaviconSet(rootDir, source, darkSource, prefix, config, t
3134
3168
  let finalSvg = svgContent;
3135
3169
  if (darkSource) {
3136
3170
  const darkPath = join(rootDir, darkSource);
3137
- if (existsSync(darkPath)) finalSvg = wrapSvgWithDarkMode(svgContent, await readFile(darkPath, "utf-8"));
3171
+ if (existsSync(darkPath)) {
3172
+ const darkSvg = await readFile(darkPath, "utf-8");
3173
+ finalSvg = wrapSvgWithDarkMode(svgContent, darkSvg);
3174
+ this.emitFile({
3175
+ type: "asset",
3176
+ fileName: `${prefix}favicon-light.svg`,
3177
+ source: svgContent
3178
+ });
3179
+ this.emitFile({
3180
+ type: "asset",
3181
+ fileName: `${prefix}favicon-dark.svg`,
3182
+ source: darkSvg
3183
+ });
3184
+ }
3138
3185
  }
3139
3186
  this.emitFile({
3140
3187
  type: "asset",
@@ -3224,8 +3271,21 @@ function faviconLinks(locale, config) {
3224
3271
  const hasLocaleOverride = locale && config.locales?.[locale];
3225
3272
  const prefix = hasLocaleOverride ? `/${locale}` : "";
3226
3273
  const isSvg = (hasLocaleOverride ? config.locales[locale].source : config.source).endsWith(".svg");
3274
+ const hasDark = !!config.darkSource;
3227
3275
  const links = [];
3228
- if (isSvg) links.push({
3276
+ if (isSvg && hasDark) links.push({
3277
+ rel: "icon",
3278
+ type: "image/svg+xml",
3279
+ href: `${prefix}/favicon-light.svg`,
3280
+ "data-favicon-theme": "light"
3281
+ }, {
3282
+ rel: "icon",
3283
+ type: "image/svg+xml",
3284
+ href: `${prefix}/favicon-dark.svg`,
3285
+ "data-favicon-theme": "dark",
3286
+ media: "not all"
3287
+ });
3288
+ else if (isSvg) links.push({
3229
3289
  rel: "icon",
3230
3290
  type: "image/svg+xml",
3231
3291
  href: `${prefix}/favicon.svg`
@@ -35,9 +35,21 @@ interface FaviconPluginConfig {
35
35
  /** Generate web manifest. Default: true */
36
36
  manifest?: boolean;
37
37
  /**
38
- * Dark mode favicon (SVG only).
39
- * When provided, the SVG favicon uses prefers-color-scheme media query
40
- * to switch between light and dark variants.
38
+ * Dark-mode favicon source.
39
+ *
40
+ * When provided, the plugin emits theme-aware `light`/`dark` variants
41
+ * (`favicon-light.svg` / `favicon-dark.svg` for SVG sources, plus the
42
+ * `*-light-*` / `*-dark-*` PNG/apple-touch set) tagged with
43
+ * `data-favicon-theme`. The injected blocking theme-swap script and
44
+ * `initTheme()` toggle their `media` attribute so the displayed
45
+ * favicon follows the app's resolved theme — including a manual
46
+ * in-app theme toggle, not just the OS `prefers-color-scheme`.
47
+ *
48
+ * For SVG sources a `favicon.svg` is also emitted that wraps both
49
+ * variants behind an OS `prefers-color-scheme` query — kept as the
50
+ * no-JS / direct-`/favicon.svg`-reference fallback only (it cannot
51
+ * follow a manual toggle, which is why the `data-favicon-theme`
52
+ * variants above are what the reactive mechanism actually uses).
41
53
  */
42
54
  darkSource?: string;
43
55
  /**
@@ -110,6 +122,8 @@ declare function faviconLinks(locale: string | undefined, config: FaviconPluginC
110
122
  type?: string;
111
123
  sizes?: string;
112
124
  href: string;
125
+ 'data-favicon-theme'?: string;
126
+ media?: string;
113
127
  }>;
114
128
  interface IcoEntry {
115
129
  buffer: Buffer;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Ambient type declarations for the custom image-import queries that
3
+ * `@pyreon/zero`'s `imagePlugin` introduces (`?optimize` / `?component`
4
+ * / `?raw`). Shipped + exported so the documented usage type-checks out
5
+ * of the box — no consumer hand-authoring required.
6
+ *
7
+ * Add ONE line to any tsconfig-covered `.d.ts` (e.g. `src/env.d.ts`):
8
+ * /// <reference types="@pyreon/zero/image-types" />
9
+ *
10
+ * Or via tsconfig.json:
11
+ * "types": ["@pyreon/zero/image-types"]
12
+ *
13
+ * This is an ambient-only **script** (no top-level import/export) so
14
+ * every `declare module` below is a global module augmentation. The
15
+ * `ProcessedImage` shape is referenced via the package self-ref
16
+ * `import('@pyreon/zero/image-plugin')` (resolution-stable in the
17
+ * published layout, and re-uses the plugin's own type so it can never
18
+ * drift out of sync).
19
+ */
20
+ //#region src/image-types.d.ts
21
+ declare module '*.jpg?optimize' {
22
+ const image: import('@pyreon/zero/image-plugin').ProcessedImage;
23
+ export default image;
24
+ }
25
+ declare module '*.jpeg?optimize' {
26
+ const image: import('@pyreon/zero/image-plugin').ProcessedImage;
27
+ export default image;
28
+ }
29
+ declare module '*.png?optimize' {
30
+ const image: import('@pyreon/zero/image-plugin').ProcessedImage;
31
+ export default image;
32
+ }
33
+ declare module '*.webp?optimize' {
34
+ const image: import('@pyreon/zero/image-plugin').ProcessedImage;
35
+ export default image;
36
+ }
37
+ declare module '*.avif?optimize' {
38
+ const image: import('@pyreon/zero/image-plugin').ProcessedImage;
39
+ export default image;
40
+ }
41
+ declare module '*.svg?component' {
42
+ const component: import('@pyreon/core').ComponentFn<{
43
+ width?: number;
44
+ height?: number;
45
+ class?: string;
46
+ style?: string;
47
+ [key: string]: unknown;
48
+ }>;
49
+ export default component;
50
+ }
51
+ declare module '*.svg?raw' {
52
+ const svg: string;
53
+ export default svg;
54
+ }
55
+ //# sourceMappingURL=image-types2.d.ts.map
@@ -1036,9 +1036,21 @@ interface FaviconPluginConfig {
1036
1036
  /** Generate web manifest. Default: true */
1037
1037
  manifest?: boolean;
1038
1038
  /**
1039
- * Dark mode favicon (SVG only).
1040
- * When provided, the SVG favicon uses prefers-color-scheme media query
1041
- * to switch between light and dark variants.
1039
+ * Dark-mode favicon source.
1040
+ *
1041
+ * When provided, the plugin emits theme-aware `light`/`dark` variants
1042
+ * (`favicon-light.svg` / `favicon-dark.svg` for SVG sources, plus the
1043
+ * `*-light-*` / `*-dark-*` PNG/apple-touch set) tagged with
1044
+ * `data-favicon-theme`. The injected blocking theme-swap script and
1045
+ * `initTheme()` toggle their `media` attribute so the displayed
1046
+ * favicon follows the app's resolved theme — including a manual
1047
+ * in-app theme toggle, not just the OS `prefers-color-scheme`.
1048
+ *
1049
+ * For SVG sources a `favicon.svg` is also emitted that wraps both
1050
+ * variants behind an OS `prefers-color-scheme` query — kept as the
1051
+ * no-JS / direct-`/favicon.svg`-reference fallback only (it cannot
1052
+ * follow a manual toggle, which is why the `data-favicon-theme`
1053
+ * variants above are what the reactive mechanism actually uses).
1042
1054
  */
1043
1055
  darkSource?: string;
1044
1056
  /**
@@ -1111,6 +1123,8 @@ declare function faviconLinks(locale: string | undefined, config: FaviconPluginC
1111
1123
  type?: string;
1112
1124
  sizes?: string;
1113
1125
  href: string;
1126
+ 'data-favicon-theme'?: string;
1127
+ media?: string;
1114
1128
  }>;
1115
1129
  //#endregion
1116
1130
  //#region src/icon.d.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/zero",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Pyreon Zero — zero-config full-stack framework powered by Pyreon and Vite",
5
5
  "license": "MIT",
6
6
  "author": "Vit Bokisch",
@@ -84,6 +84,11 @@
84
84
  "import": "./lib/image-plugin.js",
85
85
  "types": "./lib/types/image-plugin.d.ts"
86
86
  },
87
+ "./image-types": {
88
+ "bun": "./src/image-types.ts",
89
+ "import": "./lib/image-types.js",
90
+ "types": "./lib/types/image-types.d.ts"
91
+ },
87
92
  "./actions": {
88
93
  "bun": "./src/actions.ts",
89
94
  "import": "./lib/actions.js",
@@ -168,15 +173,15 @@
168
173
  "lint": "oxlint ."
169
174
  },
170
175
  "dependencies": {
171
- "@pyreon/core": "^0.20.0",
172
- "@pyreon/head": "^0.20.0",
173
- "@pyreon/meta": "^0.20.0",
174
- "@pyreon/reactivity": "^0.20.0",
175
- "@pyreon/router": "^0.20.0",
176
- "@pyreon/runtime-dom": "^0.20.0",
177
- "@pyreon/runtime-server": "^0.20.0",
178
- "@pyreon/server": "^0.20.0",
179
- "@pyreon/vite-plugin": "^0.20.0",
176
+ "@pyreon/core": "^0.21.0",
177
+ "@pyreon/head": "^0.21.0",
178
+ "@pyreon/meta": "^0.21.0",
179
+ "@pyreon/reactivity": "^0.21.0",
180
+ "@pyreon/router": "^0.21.0",
181
+ "@pyreon/runtime-dom": "^0.21.0",
182
+ "@pyreon/runtime-server": "^0.21.0",
183
+ "@pyreon/server": "^0.21.0",
184
+ "@pyreon/vite-plugin": "^0.21.0",
180
185
  "vite": "^8.0.0"
181
186
  },
182
187
  "devDependencies": {
package/src/favicon.ts CHANGED
@@ -81,9 +81,21 @@ export interface FaviconPluginConfig {
81
81
  /** Generate web manifest. Default: true */
82
82
  manifest?: boolean
83
83
  /**
84
- * Dark mode favicon (SVG only).
85
- * When provided, the SVG favicon uses prefers-color-scheme media query
86
- * to switch between light and dark variants.
84
+ * Dark-mode favicon source.
85
+ *
86
+ * When provided, the plugin emits theme-aware `light`/`dark` variants
87
+ * (`favicon-light.svg` / `favicon-dark.svg` for SVG sources, plus the
88
+ * `*-light-*` / `*-dark-*` PNG/apple-touch set) tagged with
89
+ * `data-favicon-theme`. The injected blocking theme-swap script and
90
+ * `initTheme()` toggle their `media` attribute so the displayed
91
+ * favicon follows the app's resolved theme — including a manual
92
+ * in-app theme toggle, not just the OS `prefers-color-scheme`.
93
+ *
94
+ * For SVG sources a `favicon.svg` is also emitted that wraps both
95
+ * variants behind an OS `prefers-color-scheme` query — kept as the
96
+ * no-JS / direct-`/favicon.svg`-reference fallback only (it cannot
97
+ * follow a manual toggle, which is why the `data-favicon-theme`
98
+ * variants above are what the reactive mechanism actually uses).
87
99
  */
88
100
  darkSource?: string
89
101
  /**
@@ -214,6 +226,34 @@ export function faviconPlugin(config: FaviconPluginConfig): Plugin {
214
226
  const svgPath = localeSource ? localeSource.sourcePath : sourcePath
215
227
  const isSvgSource = localeSource ? localeSource.source.endsWith('.svg') : config.source.endsWith('.svg')
216
228
 
229
+ // Serve the per-theme SVG variants (the app-toggle path):
230
+ // /favicon-light.svg → source, /favicon-dark.svg → darkSource.
231
+ // Dev-badge / devSource override applies to the light variant
232
+ // only (it is the active default the swap toggles to), matching
233
+ // the /favicon.svg handler's intent.
234
+ if (
235
+ isSvgSource &&
236
+ (svgUrl.endsWith('/favicon-light.svg') ||
237
+ svgUrl.endsWith('/favicon-dark.svg'))
238
+ ) {
239
+ const isDarkVariant = svgUrl.endsWith('/favicon-dark.svg')
240
+ const variantPath = isDarkVariant ? (darkPath ?? svgPath) : svgPath
241
+ try {
242
+ let content = await readFile(variantPath, 'utf-8')
243
+ if (!isDarkVariant) {
244
+ if (autoDevBadge) content = addDevBadgeToSvg(content)
245
+ else if (devSourcePath && existsSync(devSourcePath)) {
246
+ content = await readFile(devSourcePath, 'utf-8')
247
+ }
248
+ }
249
+ res.setHeader('Content-Type', 'image/svg+xml')
250
+ res.end(content)
251
+ return
252
+ } catch {
253
+ /* fall through */
254
+ }
255
+ }
256
+
217
257
  // Serve favicon.svg — in dev, add dev badge overlay if configured
218
258
  if (svgUrl.endsWith('/favicon.svg') && isSvgSource) {
219
259
  try {
@@ -307,8 +347,22 @@ export function faviconPlugin(config: FaviconPluginConfig): Plugin {
307
347
  injectTo: 'head'
308
348
  }> = []
309
349
 
310
- // SVG favicon (with prefers-color-scheme media query when dark variant exists)
311
- if (isSvg) {
350
+ // SVG favicon. Browsers prefer an SVG favicon over PNG when both
351
+ // are present, so the SVG link MUST carry the same
352
+ // `data-favicon-theme` contract the PNG dual-variant uses —
353
+ // otherwise the theme-swap script / initTheme() (which only touch
354
+ // `[data-favicon-theme]`) can never change the displayed icon and
355
+ // the whole reactive-favicon feature is silently dead in every
356
+ // SVG-capable browser. When a dark variant exists, emit TWO
357
+ // theme-aware SVG links (mirroring the PNG pattern); the static
358
+ // `/favicon.svg` (an OS `prefers-color-scheme` wrapped dual) stays
359
+ // emitted as the no-JS / direct-reference fallback only.
360
+ if (isSvg && hasDark) {
361
+ tags.push(
362
+ { tag: 'link', attrs: { rel: 'icon', type: 'image/svg+xml', href: '/favicon-light.svg', 'data-favicon-theme': 'light' }, injectTo: 'head' },
363
+ { tag: 'link', attrs: { rel: 'icon', type: 'image/svg+xml', href: '/favicon-dark.svg', 'data-favicon-theme': 'dark', media: 'not all' }, injectTo: 'head' },
364
+ )
365
+ } else if (isSvg) {
312
366
  tags.push({
313
367
  tag: 'link',
314
368
  attrs: { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
@@ -502,6 +556,22 @@ async function generateFaviconSet(
502
556
  if (existsSync(darkPath)) {
503
557
  const darkSvg = await readFile(darkPath, 'utf-8')
504
558
  finalSvg = wrapSvgWithDarkMode(svgContent, darkSvg)
559
+ // Per-theme SVG variants for the app-toggle path:
560
+ // transformIndexHtml / faviconLinks emit
561
+ // `/favicon-light.svg` + `/favicon-dark.svg` with
562
+ // `data-favicon-theme` so the theme-swap actually changes the
563
+ // SVG (the wrapped `favicon.svg` is OS-`prefers-color-scheme`
564
+ // only — kept above as the no-JS / direct-ref fallback).
565
+ this.emitFile({
566
+ type: 'asset',
567
+ fileName: `${prefix}favicon-light.svg`,
568
+ source: svgContent,
569
+ })
570
+ this.emitFile({
571
+ type: 'asset',
572
+ fileName: `${prefix}favicon-dark.svg`,
573
+ source: darkSvg,
574
+ })
505
575
  }
506
576
  }
507
577
 
@@ -599,14 +669,39 @@ async function generateFaviconSet(
599
669
  export function faviconLinks(
600
670
  locale: string | undefined,
601
671
  config: FaviconPluginConfig,
602
- ): Array<{ rel: string; type?: string; sizes?: string; href: string }> {
672
+ ): Array<{
673
+ rel: string
674
+ type?: string
675
+ sizes?: string
676
+ href: string
677
+ 'data-favicon-theme'?: string
678
+ media?: string
679
+ }> {
603
680
  const hasLocaleOverride = locale && config.locales?.[locale]
604
681
  const prefix = hasLocaleOverride ? `/${locale}` : ''
605
682
  const isSvg = (hasLocaleOverride ? config.locales![locale]!.source : config.source).endsWith('.svg')
606
-
607
- const links: Array<{ rel: string; type?: string; sizes?: string; href: string }> = []
608
-
609
- if (isSvg) {
683
+ const hasDark = !!config.darkSource
684
+
685
+ const links: Array<{
686
+ rel: string
687
+ type?: string
688
+ sizes?: string
689
+ href: string
690
+ 'data-favicon-theme'?: string
691
+ media?: string
692
+ }> = []
693
+
694
+ // Mirror transformIndexHtml: a single static SVG link would always
695
+ // win over the theme-toggled PNGs (browsers prefer SVG), silently
696
+ // killing reactive switching for SSR'd pages too. Emit the two
697
+ // theme-aware SVG variants so initTheme()'s `[data-favicon-theme]`
698
+ // swap reaches the SVG. `/favicon.svg` stays the no-JS fallback.
699
+ if (isSvg && hasDark) {
700
+ links.push(
701
+ { rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon-light.svg`, 'data-favicon-theme': 'light' },
702
+ { rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon-dark.svg`, 'data-favicon-theme': 'dark', media: 'not all' },
703
+ )
704
+ } else if (isSvg) {
610
705
  links.push({ rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon.svg` })
611
706
  }
612
707
 
@@ -23,7 +23,13 @@ function warnSharpMissing() {
23
23
  //
24
24
  // Usage in code:
25
25
  // import heroImg from "./hero.jpg?optimize"
26
- // // → { src, srcset, width, height, placeholder }
26
+ // // → ProcessedImage { src, srcset, width, height, placeholder }
27
+ //
28
+ // Type the `?optimize` / `?component` / `?raw` imports out of the box —
29
+ // add ONE line to a tsconfig-covered `.d.ts` (e.g. `src/env.d.ts`):
30
+ // /// <reference types="@pyreon/zero/image-types" />
31
+ // (ships the ambient `declare module "*?optimize"` etc. — reuses this
32
+ // module's own `ProcessedImage`, so it never drifts.)
27
33
  //
28
34
  // Or use the component helper:
29
35
  // import { Image } from "@pyreon/zero/image"
@@ -389,7 +395,15 @@ async function loadDevImage(
389
395
  placeholderSize: number,
390
396
  ): Promise<ProcessedImage> {
391
397
  const metadata = await getImageMetadata(absPath)
392
- const publicPath = rawPath.startsWith('/') ? rawPath : `/@fs/${absPath}`
398
+ // `rawPath` is a public-dir web path (e.g. `/logo.png`, served from
399
+ // `public/` at the web root) ONLY when it does NOT resolve to a real
400
+ // file on disk — the same discriminator the `absPath` derivation uses
401
+ // above. `resolveId` now hands absolute fs paths for relative/aliased
402
+ // imports (`/Users/…/img.png`); those ARE real files and must be
403
+ // served through Vite's `/@fs/` prefix, not as a literal `/Users/…`
404
+ // URL (which 404s in dev — build mode was unaffected).
405
+ const isPublicWebPath = rawPath.startsWith('/') && !existsSync(rawPath)
406
+ const publicPath = isPublicWebPath ? rawPath : `/@fs/${absPath}`
393
407
 
394
408
  return {
395
409
  src: publicPath,
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Ambient type declarations for the custom image-import queries that
3
+ * `@pyreon/zero`'s `imagePlugin` introduces (`?optimize` / `?component`
4
+ * / `?raw`). Shipped + exported so the documented usage type-checks out
5
+ * of the box — no consumer hand-authoring required.
6
+ *
7
+ * Add ONE line to any tsconfig-covered `.d.ts` (e.g. `src/env.d.ts`):
8
+ * /// <reference types="@pyreon/zero/image-types" />
9
+ *
10
+ * Or via tsconfig.json:
11
+ * "types": ["@pyreon/zero/image-types"]
12
+ *
13
+ * This is an ambient-only **script** (no top-level import/export) so
14
+ * every `declare module` below is a global module augmentation. The
15
+ * `ProcessedImage` shape is referenced via the package self-ref
16
+ * `import('@pyreon/zero/image-plugin')` (resolution-stable in the
17
+ * published layout, and re-uses the plugin's own type so it can never
18
+ * drift out of sync).
19
+ */
20
+
21
+ declare module '*.jpg?optimize' {
22
+ const image: import('@pyreon/zero/image-plugin').ProcessedImage
23
+ export default image
24
+ }
25
+
26
+ declare module '*.jpeg?optimize' {
27
+ const image: import('@pyreon/zero/image-plugin').ProcessedImage
28
+ export default image
29
+ }
30
+
31
+ declare module '*.png?optimize' {
32
+ const image: import('@pyreon/zero/image-plugin').ProcessedImage
33
+ export default image
34
+ }
35
+
36
+ declare module '*.webp?optimize' {
37
+ const image: import('@pyreon/zero/image-plugin').ProcessedImage
38
+ export default image
39
+ }
40
+
41
+ declare module '*.avif?optimize' {
42
+ const image: import('@pyreon/zero/image-plugin').ProcessedImage
43
+ export default image
44
+ }
45
+
46
+ declare module '*.svg?component' {
47
+ const component: import('@pyreon/core').ComponentFn<{
48
+ width?: number
49
+ height?: number
50
+ class?: string
51
+ style?: string
52
+ [key: string]: unknown
53
+ }>
54
+ export default component
55
+ }
56
+
57
+ declare module '*.svg?raw' {
58
+ const svg: string
59
+ export default svg
60
+ }
@@ -1,51 +0,0 @@
1
- /**
2
- * Type declarations for image imports processed by @pyreon/zero's imagePlugin.
3
- *
4
- * Add to your tsconfig.json:
5
- * "types": ["@pyreon/zero/image-types"]
6
- *
7
- * Or reference directly:
8
- * /// <reference types="@pyreon/zero/image-types" />
9
- */
10
-
11
- declare module '*.jpg?optimize' {
12
- const image: import('./image-plugin').ProcessedImage
13
- export default image
14
- }
15
-
16
- declare module '*.jpeg?optimize' {
17
- const image: import('./image-plugin').ProcessedImage
18
- export default image
19
- }
20
-
21
- declare module '*.png?optimize' {
22
- const image: import('./image-plugin').ProcessedImage
23
- export default image
24
- }
25
-
26
- declare module '*.webp?optimize' {
27
- const image: import('./image-plugin').ProcessedImage
28
- export default image
29
- }
30
-
31
- declare module '*.avif?optimize' {
32
- const image: import('./image-plugin').ProcessedImage
33
- export default image
34
- }
35
-
36
- declare module '*.svg?component' {
37
- import type { ComponentFn } from '@pyreon/core'
38
- const component: ComponentFn<{
39
- width?: number
40
- height?: number
41
- class?: string
42
- style?: string
43
- [key: string]: unknown
44
- }>
45
- export default component
46
- }
47
-
48
- declare module '*.svg?raw' {
49
- const svg: string
50
- export default svg
51
- }