@pyreon/zero 0.12.12 → 0.12.14

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.
@@ -1 +1 @@
1
- {"version":3,"file":"i18n-routing.js","names":[],"sources":["../src/i18n-routing.ts"],"sourcesContent":["import { createContext } from '@pyreon/core'\nimport { signal } from '@pyreon/reactivity'\nimport type { Plugin } from 'vite'\n\n// ─── Localized routing ─────────────────────────────────────────────────────\n//\n// Adds locale-prefixed routes to Zero's file-system router:\n// - /about → /en/about, /de/about, /cs/about\n// - / → /en, /de, /cs (or default locale without prefix)\n// - Automatic locale detection from Accept-Language header\n// - Redirect to preferred locale\n// - hreflang link generation\n//\n// Usage:\n// import { i18nRouting } from \"@pyreon/zero\"\n// export default { plugins: [zero(), i18nRouting({ locales: [\"en\", \"de\"], defaultLocale: \"en\" })] }\n\nexport interface I18nRoutingConfig {\n /** Supported locales. e.g. [\"en\", \"de\", \"cs\"] */\n locales: string[]\n /** Default locale — served without prefix (/ instead of /en/). */\n defaultLocale: string\n /** Redirect root to detected locale. Default: true */\n detectLocale?: boolean\n /** Cookie name to persist locale preference. Default: \"locale\" */\n cookieName?: string\n /** URL strategy. Default: \"prefix-except-default\" */\n strategy?: 'prefix' | 'prefix-except-default'\n}\n\nexport interface LocaleContext {\n /** Current locale code. e.g. \"en\", \"de\" */\n locale: string\n /** All supported locales. */\n locales: string[]\n /** Default locale. */\n defaultLocale: string\n /** Build a localized path. e.g. localePath(\"/about\", \"de\") → \"/de/about\" */\n localePath: (path: string, locale?: string) => string\n /** Get hreflang alternates for the current path. */\n alternates: () => Array<{ locale: string; url: string }>\n}\n\n/**\n * Detect preferred locale from Accept-Language header.\n */\nexport function detectLocaleFromHeader(\n acceptLanguage: string | null | undefined,\n locales: string[],\n defaultLocale: string,\n): string {\n if (!acceptLanguage) return defaultLocale\n\n // Parse Accept-Language: en-US,en;q=0.9,de;q=0.8\n const preferred = acceptLanguage\n .split(',')\n .map((part) => {\n const [lang, q] = part.trim().split(';q=')\n return {\n lang: lang?.split('-')[0]?.toLowerCase() ?? '',\n quality: q ? Number.parseFloat(q) : 1,\n }\n })\n .sort((a, b) => b.quality - a.quality)\n\n for (const { lang } of preferred) {\n if (locales.includes(lang)) return lang\n }\n\n return defaultLocale\n}\n\n/**\n * Extract locale from a URL path.\n * Returns { locale, pathWithoutLocale }.\n */\nexport function extractLocaleFromPath(\n path: string,\n locales: string[],\n defaultLocale: string,\n): { locale: string; pathWithoutLocale: string } {\n const segments = path.split('/').filter(Boolean)\n const firstSegment = segments[0]?.toLowerCase()\n\n if (firstSegment && locales.includes(firstSegment)) {\n return {\n locale: firstSegment,\n pathWithoutLocale: '/' + segments.slice(1).join('/') || '/',\n }\n }\n\n return { locale: defaultLocale, pathWithoutLocale: path }\n}\n\n/**\n * Build a localized path.\n */\nexport function buildLocalePath(\n path: string,\n locale: string,\n defaultLocale: string,\n strategy: 'prefix' | 'prefix-except-default',\n): string {\n const clean = path === '/' ? '' : path\n if (strategy === 'prefix-except-default' && locale === defaultLocale) {\n return path\n }\n return `/${locale}${clean}`\n}\n\n/**\n * Create a LocaleContext for use in components and loaders.\n */\nexport function createLocaleContext(\n locale: string,\n path: string,\n config: I18nRoutingConfig,\n): LocaleContext {\n const strategy = config.strategy ?? 'prefix-except-default'\n\n return {\n locale,\n locales: config.locales,\n defaultLocale: config.defaultLocale,\n\n localePath(targetPath: string, targetLocale?: string) {\n return buildLocalePath(\n targetPath,\n targetLocale ?? locale,\n config.defaultLocale,\n strategy,\n )\n },\n\n alternates() {\n const { pathWithoutLocale } = extractLocaleFromPath(\n path,\n config.locales,\n config.defaultLocale,\n )\n return config.locales.map((loc) => ({\n locale: loc,\n url: buildLocalePath(pathWithoutLocale, loc, config.defaultLocale, strategy),\n }))\n },\n }\n}\n\n/**\n * I18n routing middleware for Zero's server.\n *\n * - Detects locale from URL prefix or Accept-Language header\n * - Redirects root to preferred locale (when detectLocale is true)\n * - Sets locale context for loaders and components\n *\n * @example\n * ```ts\n * // zero.config.ts\n * import { i18nRouting } from \"@pyreon/zero\"\n *\n * export default defineConfig({\n * plugins: [\n * i18nRouting({\n * locales: [\"en\", \"de\", \"cs\"],\n * defaultLocale: \"en\",\n * }),\n * ],\n * })\n * ```\n */\nexport function i18nRouting(config: I18nRoutingConfig): Plugin {\n const strategy = config.strategy ?? 'prefix-except-default'\n const detectEnabled = config.detectLocale !== false\n const cookieName = config.cookieName ?? 'locale'\n\n return {\n name: 'pyreon-zero-i18n-routing',\n\n // Route duplication is NOT handled here. The fs-router's `scanRouteFiles`\n // consumes the i18n config to duplicate routes per locale at build time.\n // This plugin only provides: (1) the server middleware for locale detection\n // and (2) the runtime hooks (useLocale, setLocale) for client-side use.\n configResolved() {},\n\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const url = req.url ?? '/'\n\n // Skip static assets\n if (url.startsWith('/@') || url.startsWith('/__') || url.includes('.')) {\n return next()\n }\n\n const { locale } = extractLocaleFromPath(\n url,\n config.locales,\n config.defaultLocale,\n )\n\n // Redirect root to detected locale\n if (detectEnabled && url === '/') {\n const cookies = parseCookies(req.headers.cookie)\n const preferredFromCookie = cookies[cookieName]\n const preferredFromHeader = detectLocaleFromHeader(\n req.headers['accept-language'],\n config.locales,\n config.defaultLocale,\n )\n const preferred = preferredFromCookie && config.locales.includes(preferredFromCookie)\n ? preferredFromCookie\n : preferredFromHeader\n\n if (strategy === 'prefix' || preferred !== config.defaultLocale) {\n res.writeHead(302, { Location: `/${preferred}/` })\n res.end()\n return\n }\n }\n\n // Attach locale context to request for loaders\n ;(req as any).__locale = locale\n ;(req as any).__localeContext = createLocaleContext(locale, url, config)\n\n // Update the module-level signal so useLocale() returns the correct value\n localeSignal.set(locale)\n\n next()\n })\n },\n }\n}\n\nfunction parseCookies(header: string | undefined): Record<string, string> {\n if (!header) return {}\n const result: Record<string, string> = {}\n for (const pair of header.split(';')) {\n const [key, value] = pair.trim().split('=')\n if (key && value) result[key] = decodeURIComponent(value)\n }\n return result\n}\n\n// ─── Reactive locale hook ───────────────────────────────────────────────────\n\n/** @internal Context for the current locale. */\nexport const LocaleCtx = createContext<string>('en')\n\n/** Current locale signal — set by the server middleware or client-side detection. */\nexport const localeSignal = signal('en')\n\n/**\n * Read the current locale reactively.\n *\n * Returns the locale signal value directly — reactive in both SSR and CSR.\n * The server middleware sets `localeSignal` per-request, and client-side\n * `setLocale()` updates it as well.\n *\n * @example\n * ```tsx\n * const locale = useLocale() // \"en\", \"de\", etc.\n * ```\n */\nexport function useLocale(): string {\n return localeSignal()\n}\n\n/**\n * Set the locale client-side and update the URL.\n *\n * @example\n * ```tsx\n * <button onClick={() => setLocale('de')}>Deutsch</button>\n * ```\n */\nexport function setLocale(\n locale: string,\n config: I18nRoutingConfig,\n): void {\n localeSignal.set(locale)\n\n // Persist to cookie\n if (typeof document !== 'undefined') {\n document.cookie = `${config.cookieName ?? 'locale'}=${locale}; path=/; max-age=31536000`\n }\n\n // Navigate to localized URL — use pushState to avoid full page reload\n if (typeof window !== 'undefined') {\n const strategy = config.strategy ?? 'prefix-except-default'\n const { pathWithoutLocale } = extractLocaleFromPath(\n window.location.pathname,\n config.locales,\n config.defaultLocale,\n )\n const newPath = buildLocalePath(pathWithoutLocale, locale, config.defaultLocale, strategy)\n window.history.pushState(null, '', newPath)\n // Dispatch popstate so @pyreon/router picks up the URL change\n window.dispatchEvent(new PopStateEvent('popstate'))\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"i18n-routing.js","names":[],"sources":["../src/i18n-routing.ts"],"sourcesContent":["import { createContext } from '@pyreon/core'\nimport { signal } from '@pyreon/reactivity'\nimport type { Plugin } from 'vite'\n\n// ─── Localized routing ─────────────────────────────────────────────────────\n//\n// Adds locale-prefixed routes to Zero's file-system router:\n// - /about → /en/about, /de/about, /cs/about\n// - / → /en, /de, /cs (or default locale without prefix)\n// - Automatic locale detection from Accept-Language header\n// - Redirect to preferred locale\n// - hreflang link generation\n//\n// Usage:\n// import { i18nRouting } from \"@pyreon/zero\"\n// export default { plugins: [Pyreon], defaultLocale: \"en\" })] }\n\nexport interface I18nRoutingConfig {\n /** Supported locales. e.g. [\"en\", \"de\", \"cs\"] */\n locales: string[]\n /** Default locale — served without prefix (/ instead of /en/). */\n defaultLocale: string\n /** Redirect root to detected locale. Default: true */\n detectLocale?: boolean\n /** Cookie name to persist locale preference. Default: \"locale\" */\n cookieName?: string\n /** URL strategy. Default: \"prefix-except-default\" */\n strategy?: 'prefix' | 'prefix-except-default'\n}\n\nexport interface LocaleContext {\n /** Current locale code. e.g. \"en\", \"de\" */\n locale: string\n /** All supported locales. */\n locales: string[]\n /** Default locale. */\n defaultLocale: string\n /** Build a localized path. e.g. localePath(\"/about\", \"de\") → \"/de/about\" */\n localePath: (path: string, locale?: string) => string\n /** Get hreflang alternates for the current path. */\n alternates: () => Array<{ locale: string; url: string }>\n}\n\n/**\n * Detect preferred locale from Accept-Language header.\n */\nexport function detectLocaleFromHeader(\n acceptLanguage: string | null | undefined,\n locales: string[],\n defaultLocale: string,\n): string {\n if (!acceptLanguage) return defaultLocale\n\n // Parse Accept-Language: en-US,en;q=0.9,de;q=0.8\n const preferred = acceptLanguage\n .split(',')\n .map((part) => {\n const [lang, q] = part.trim().split(';q=')\n return {\n lang: lang?.split('-')[0]?.toLowerCase() ?? '',\n quality: q ? Number.parseFloat(q) : 1,\n }\n })\n .sort((a, b) => b.quality - a.quality)\n\n for (const { lang } of preferred) {\n if (locales.includes(lang)) return lang\n }\n\n return defaultLocale\n}\n\n/**\n * Extract locale from a URL path.\n * Returns { locale, pathWithoutLocale }.\n */\nexport function extractLocaleFromPath(\n path: string,\n locales: string[],\n defaultLocale: string,\n): { locale: string; pathWithoutLocale: string } {\n const segments = path.split('/').filter(Boolean)\n const firstSegment = segments[0]?.toLowerCase()\n\n if (firstSegment && locales.includes(firstSegment)) {\n return {\n locale: firstSegment,\n pathWithoutLocale: '/' + segments.slice(1).join('/') || '/',\n }\n }\n\n return { locale: defaultLocale, pathWithoutLocale: path }\n}\n\n/**\n * Build a localized path.\n */\nexport function buildLocalePath(\n path: string,\n locale: string,\n defaultLocale: string,\n strategy: 'prefix' | 'prefix-except-default',\n): string {\n const clean = path === '/' ? '' : path\n if (strategy === 'prefix-except-default' && locale === defaultLocale) {\n return path\n }\n return `/${locale}${clean}`\n}\n\n/**\n * Create a LocaleContext for use in components and loaders.\n */\nexport function createLocaleContext(\n locale: string,\n path: string,\n config: I18nRoutingConfig,\n): LocaleContext {\n const strategy = config.strategy ?? 'prefix-except-default'\n\n return {\n locale,\n locales: config.locales,\n defaultLocale: config.defaultLocale,\n\n localePath(targetPath: string, targetLocale?: string) {\n return buildLocalePath(\n targetPath,\n targetLocale ?? locale,\n config.defaultLocale,\n strategy,\n )\n },\n\n alternates() {\n const { pathWithoutLocale } = extractLocaleFromPath(\n path,\n config.locales,\n config.defaultLocale,\n )\n return config.locales.map((loc) => ({\n locale: loc,\n url: buildLocalePath(pathWithoutLocale, loc, config.defaultLocale, strategy),\n }))\n },\n }\n}\n\n/**\n * I18n routing middleware for Zero's server.\n *\n * - Detects locale from URL prefix or Accept-Language header\n * - Redirects root to preferred locale (when detectLocale is true)\n * - Sets locale context for loaders and components\n *\n * @example\n * ```ts\n * // zero.config.ts\n * import { i18nRouting } from \"@pyreon/zero\"\n *\n * export default defineConfig({\n * plugins: [\n * i18nRouting({\n * locales: [\"en\", \"de\", \"cs\"],\n * defaultLocale: \"en\",\n * }),\n * ],\n * })\n * ```\n */\nexport function i18nRouting(config: I18nRoutingConfig): Plugin {\n const strategy = config.strategy ?? 'prefix-except-default'\n const detectEnabled = config.detectLocale !== false\n const cookieName = config.cookieName ?? 'locale'\n\n return {\n name: 'pyreon-zero-i18n-routing',\n\n // Route duplication is NOT handled here. The fs-router's `scanRouteFiles`\n // consumes the i18n config to duplicate routes per locale at build time.\n // This plugin only provides: (1) the server middleware for locale detection\n // and (2) the runtime hooks (useLocale, setLocale) for client-side use.\n configResolved() {},\n\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const url = req.url ?? '/'\n\n // Skip static assets\n if (url.startsWith('/@') || url.startsWith('/__') || url.includes('.')) {\n return next()\n }\n\n const { locale } = extractLocaleFromPath(\n url,\n config.locales,\n config.defaultLocale,\n )\n\n // Redirect root to detected locale\n if (detectEnabled && url === '/') {\n const cookies = parseCookies(req.headers.cookie)\n const preferredFromCookie = cookies[cookieName]\n const preferredFromHeader = detectLocaleFromHeader(\n req.headers['accept-language'],\n config.locales,\n config.defaultLocale,\n )\n const preferred = preferredFromCookie && config.locales.includes(preferredFromCookie)\n ? preferredFromCookie\n : preferredFromHeader\n\n if (strategy === 'prefix' || preferred !== config.defaultLocale) {\n res.writeHead(302, { Location: `/${preferred}/` })\n res.end()\n return\n }\n }\n\n // Attach locale context to request for loaders\n ;(req as any).__locale = locale\n ;(req as any).__localeContext = createLocaleContext(locale, url, config)\n\n // Update the module-level signal so useLocale() returns the correct value\n localeSignal.set(locale)\n\n next()\n })\n },\n }\n}\n\nfunction parseCookies(header: string | undefined): Record<string, string> {\n if (!header) return {}\n const result: Record<string, string> = {}\n for (const pair of header.split(';')) {\n const [key, value] = pair.trim().split('=')\n if (key && value) result[key] = decodeURIComponent(value)\n }\n return result\n}\n\n// ─── Reactive locale hook ───────────────────────────────────────────────────\n\n/** @internal Context for the current locale. */\nexport const LocaleCtx = createContext<string>('en')\n\n/** Current locale signal — set by the server middleware or client-side detection. */\nexport const localeSignal = signal('en')\n\n/**\n * Read the current locale reactively.\n *\n * Returns the locale signal value directly — reactive in both SSR and CSR.\n * The server middleware sets `localeSignal` per-request, and client-side\n * `setLocale()` updates it as well.\n *\n * @example\n * ```tsx\n * const locale = useLocale() // \"en\", \"de\", etc.\n * ```\n */\nexport function useLocale(): string {\n return localeSignal()\n}\n\n/**\n * Set the locale client-side and update the URL.\n *\n * @example\n * ```tsx\n * <button onClick={() => setLocale('de')}>Deutsch</button>\n * ```\n */\nexport function setLocale(\n locale: string,\n config: I18nRoutingConfig,\n): void {\n localeSignal.set(locale)\n\n // Persist to cookie\n if (typeof document !== 'undefined') {\n document.cookie = `${config.cookieName ?? 'locale'}=${locale}; path=/; max-age=31536000`\n }\n\n // Navigate to localized URL — use pushState to avoid full page reload\n if (typeof window !== 'undefined') {\n const strategy = config.strategy ?? 'prefix-except-default'\n const { pathWithoutLocale } = extractLocaleFromPath(\n window.location.pathname,\n config.locales,\n config.defaultLocale,\n )\n const newPath = buildLocalePath(pathWithoutLocale, locale, config.defaultLocale, strategy)\n window.history.pushState(null, '', newPath)\n // Dispatch popstate so @pyreon/router picks up the URL change\n window.dispatchEvent(new PopStateEvent('popstate'))\n }\n}\n"],"mappings":"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"}
@@ -7,7 +7,7 @@ let sharpWarned = false;
7
7
  function warnSharpMissing() {
8
8
  if (sharpWarned) return;
9
9
  sharpWarned = true;
10
- console.warn("\n[zero:image] sharp not installed — images will not be optimized. Install for full support: bun add -D sharp\n");
10
+ console.warn("\n[Pyreon] sharp not installed — images will not be optimized. Install for full support: bun add -D sharp\n");
11
11
  }
12
12
  /** Built-in CDN providers. */
13
13
  const cdnProviders = {
@@ -1 +1 @@
1
- {"version":3,"file":"image-plugin.js","names":[],"sources":["../src/image-plugin.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { basename, extname, join } from 'node:path'\nimport type { Plugin } from 'vite'\n\nlet sharpWarned = false\nfunction warnSharpMissing() {\n if (sharpWarned) return\n sharpWarned = true\n // oxlint-disable-next-line no-console\n console.warn(\n '\\n[zero:image] sharp not installed — images will not be optimized. Install for full support: bun add -D sharp\\n',\n )\n}\n\n// ─── Image processing Vite plugin ──────────────────────────────────────────\n//\n// Processes images at build time:\n// - Generates multiple sizes for responsive srcset\n// - Converts to modern formats (WebP, AVIF)\n// - Creates tiny blur placeholders (base64 inline)\n// - Outputs optimized images to the build directory\n//\n// Usage in code:\n// import heroImg from \"./hero.jpg?optimize\"\n// // → { src, srcset, width, height, placeholder }\n//\n// Or use the component helper:\n// import { Image } from \"@pyreon/zero/image\"\n// <Image src=\"/hero.jpg\" width={1920} height={1080} optimize />\n\n/**\n * CDN provider — rewrites image URLs to CDN endpoints.\n * Return the rewritten URL, or null to use local processing.\n */\nexport type ImageCdnProvider = (src: string, opts: {\n width: number\n quality: number\n format: ImageFormat\n}) => string | null\n\n/** Built-in CDN providers. */\nexport const cdnProviders = {\n /** Cloudinary: `https://res.cloudinary.com/{cloud}/image/upload/...` */\n cloudinary: (cloudName: string): ImageCdnProvider => (src, { width, quality, format }) =>\n `https://res.cloudinary.com/${cloudName}/image/upload/w_${width},q_${quality},f_${format}/${src}`,\n\n /** Imgix: `https://{domain}.imgix.net/...?w=...&q=...&fm=...` */\n imgix: (domain: string): ImageCdnProvider => (src, { width, quality, format }) =>\n `https://${domain}.imgix.net/${src}?w=${width}&q=${quality}&fm=${format}&auto=format`,\n\n /** Vercel Image Optimization: `/_next/image?url=...&w=...&q=...` */\n vercel: (): ImageCdnProvider => (src, { width, quality }) =>\n `/_vercel/image?url=${encodeURIComponent(src)}&w=${width}&q=${quality}`,\n\n /** Bunny CDN: `https://{pullZone}.b-cdn.net/...?width=...&quality=...` */\n bunny: (pullZone: string): ImageCdnProvider => (src, { width, quality }) =>\n `https://${pullZone}.b-cdn.net/${src}?width=${width}&quality=${quality}`,\n} as const\n\n/** Placeholder generation strategy. */\nexport type PlaceholderStrategy = 'blur' | 'dominant-color' | 'none'\n\n/** SVG processing options for ?component imports. */\nexport interface SvgOptions {\n /** Replace fill/stroke with currentColor. Default: true */\n currentColor?: boolean\n /** Default size (width/height). */\n defaultSize?: number\n}\n\nexport interface ImagePluginConfig {\n /** Output directory for processed images. Default: \"assets/img\" */\n outDir?: string\n /** Default widths for responsive images. Default: [640, 1024, 1920] */\n widths?: number[]\n /** Output formats. Default: [\"webp\"] */\n formats?: ImageFormat[]\n /** Quality for lossy formats (1-100). Default: 80 */\n quality?: number\n /** Blur placeholder size in px. Default: 16 */\n placeholderSize?: number\n /** Placeholder strategy. Default: \"blur\" */\n placeholder?: PlaceholderStrategy\n /** File patterns to process. Default: /\\.(jpe?g|png|webp|avif)$/i */\n include?: RegExp\n /**\n * CDN provider for URL rewriting. When set, images are NOT processed\n * locally — URLs are rewritten to the CDN endpoint.\n *\n * @example\n * ```ts\n * imagePlugin({ cdn: cdnProviders.cloudinary('my-cloud') })\n * imagePlugin({ cdn: cdnProviders.vercel() })\n * ```\n */\n cdn?: ImageCdnProvider\n /**\n * SVG processing options. Enables `?component` import for inline SVGs.\n *\n * @example\n * ```tsx\n * import Logo from './logo.svg?component'\n * <Logo width={24} class=\"text-primary\" />\n * ```\n */\n svg?: SvgOptions | boolean\n}\n\nexport type ImageFormat = 'webp' | 'avif' | 'jpeg' | 'png'\n\n/** Per-format source set for <picture> <source> elements. */\nexport interface FormatSource {\n /** MIME type. e.g. \"image/webp\", \"image/avif\" */\n type: string\n /** srcset string for this format. e.g. \"/img-640.webp 640w, /img-1920.webp 1920w\" */\n srcset: string\n}\n\nexport interface ProcessedImage {\n /** Fallback source path (last format, largest width). */\n src: string\n /** Fallback srcset string (last format). */\n srcset: string\n /** Intrinsic width. */\n width: number\n /** Intrinsic height. */\n height: number\n /** Base64 blur placeholder data URI. */\n placeholder: string\n /** Per-format source sets for <picture> element. Ordered by priority (best format first). */\n formats: FormatSource[]\n /** Flat list of all sources. */\n sources: Array<{ src: string; width: number; format: string }>\n}\n\nconst IMAGE_EXT_RE = /\\.(jpe?g|png|webp|avif)$/i\n\n/**\n * Zero image processing Vite plugin.\n *\n * Transforms image imports with query params into optimized responsive images:\n *\n * @example\n * // vite.config.ts\n * import { imagePlugin } from \"@pyreon/zero/image-plugin\"\n *\n * export default {\n * plugins: [\n * pyreon(),\n * zero(),\n * imagePlugin({ widths: [480, 960, 1440], quality: 85 }),\n * ],\n * }\n *\n * @example\n * // In a component — import with ?optimize\n * import hero from \"./images/hero.jpg?optimize\"\n * // hero = { src, srcset, width, height, placeholder }\n *\n * <Image {...hero} alt=\"Hero\" priority />\n */\nexport function imagePlugin(config: ImagePluginConfig = {}): Plugin {\n const defaultWidths = config.widths ?? [640, 1024, 1920]\n const defaultFormats = config.formats ?? ['webp']\n const quality = config.quality ?? 80\n const placeholderSize = config.placeholderSize ?? 16\n const placeholderStrategy = config.placeholder ?? 'blur'\n const outSubDir = config.outDir ?? 'assets/img'\n const include = config.include ?? IMAGE_EXT_RE\n const cdn = config.cdn\n const svgOpts: SvgOptions | false = config.svg === true\n ? { currentColor: true }\n : config.svg === false || config.svg === undefined\n ? false\n : config.svg\n\n let root = ''\n let outDir = ''\n let isBuild = false\n\n return {\n name: 'pyreon-zero-images',\n enforce: 'pre',\n\n configResolved(resolvedConfig) {\n root = resolvedConfig.root\n outDir = resolvedConfig.build.outDir\n isBuild = resolvedConfig.command === 'build'\n },\n\n async resolveId(id) {\n // SVG as component: import Logo from './logo.svg?component'\n if (svgOpts && id.includes('?component') && id.split('?')[0]!.endsWith('.svg')) {\n return `\\0virtual:zero-svg:${id}`\n }\n // Handle ?optimize query on image imports\n if (id.includes('?optimize') && include.test(id.split('?')[0]!)) {\n return `\\0virtual:zero-image:${id}`\n }\n return null\n },\n\n async load(id) {\n // SVG component loading\n if (id.startsWith('\\0virtual:zero-svg:')) {\n const rawPath = id.replace('\\0virtual:zero-svg:', '').split('?')[0] ?? id\n const absPath = rawPath.startsWith('/') ? join(root, rawPath) : rawPath\n if (!existsSync(absPath)) return null\n\n let svg = await readFile(absPath, 'utf-8')\n\n // Replace fill/stroke with currentColor\n if (svgOpts && (svgOpts as SvgOptions).currentColor !== false) {\n svg = svg\n .replace(/fill=\"(?!none)[^\"]*\"/g, 'fill=\"currentColor\"')\n .replace(/stroke=\"(?!none)[^\"]*\"/g, 'stroke=\"currentColor\"')\n }\n\n // Add default size from config\n const defaultSize = svgOpts && (svgOpts as SvgOptions).defaultSize\n if (defaultSize && !svg.includes('width=')) {\n svg = svg.replace('<svg', `<svg width=\"${defaultSize}\" height=\"${defaultSize}\"`)\n }\n\n // Export as Pyreon component\n return `\nimport { h } from '@pyreon/core'\nconst _svg = ${JSON.stringify(svg)}\nexport default function SvgComponent(props) {\n const el = h('span', {\n ...props,\n dangerouslySetInnerHTML: { __html: _svg },\n style: [\n 'display:inline-flex;align-items:center;justify-content:center',\n props.width ? 'width:' + props.width + 'px' : '',\n props.height ? 'height:' + props.height + 'px' : '',\n props.style || '',\n ].filter(Boolean).join(';'),\n })\n return el\n}\n`\n }\n\n // Image optimization loading\n if (!id.startsWith('\\0virtual:zero-image:')) return null\n\n const rawPath = id.replace('\\0virtual:zero-image:', '').split('?')[0] ?? id\n const absPath = rawPath.startsWith('/') ? join(root, 'public', rawPath) : rawPath\n\n // CDN mode — rewrite URLs, no local processing\n if (cdn) {\n const metadata = await getImageMetadata(absPath)\n const sources = defaultWidths.map((w) => ({\n src: cdn(rawPath, { width: w, quality, format: defaultFormats[0]! }) ?? rawPath,\n width: w,\n format: defaultFormats[0]! as string,\n }))\n const srcset = sources.map((s) => `${s.src} ${s.width}w`).join(', ')\n const result: ProcessedImage = {\n src: sources[sources.length - 1]?.src ?? rawPath,\n srcset,\n width: metadata.width,\n height: metadata.height,\n placeholder: placeholderStrategy === 'none' ? ''\n : await generateBlurPlaceholder(absPath, placeholderSize),\n formats: defaultFormats.map((fmt) => ({\n type: `image/${fmt}`,\n srcset: defaultWidths\n .map((w) => `${cdn(rawPath, { width: w, quality, format: fmt }) ?? rawPath} ${w}w`)\n .join(', '),\n })),\n sources,\n }\n return `export default ${JSON.stringify(result)}`\n }\n\n if (!isBuild) {\n const result = await loadDevImage(absPath, rawPath, placeholderSize)\n return `export default ${JSON.stringify(result)}`\n }\n\n const processed = await processImage(absPath, {\n widths: defaultWidths,\n formats: defaultFormats,\n quality,\n placeholderSize,\n outSubDir,\n outDir: join(root, outDir),\n })\n\n await emitProcessedSources(processed, outSubDir, this)\n rebuildFormatSrcsets(processed, absPath)\n\n return `export default ${JSON.stringify(processed)}`\n },\n }\n}\n\nasync function loadDevImage(\n absPath: string,\n rawPath: string,\n placeholderSize: number,\n): Promise<ProcessedImage> {\n const metadata = await getImageMetadata(absPath)\n const publicPath = rawPath.startsWith('/') ? rawPath : `/@fs/${absPath}`\n\n return {\n src: publicPath,\n srcset: '',\n width: metadata.width,\n height: metadata.height,\n placeholder: await generateBlurPlaceholder(absPath, placeholderSize),\n formats: [],\n sources: [{ src: publicPath, width: metadata.width, format: 'original' }],\n }\n}\n\nasync function emitProcessedSources(\n processed: ProcessedImage,\n outSubDir: string,\n ctx: {\n emitFile: (f: { type: 'asset'; fileName: string; source: Uint8Array }) => void\n },\n) {\n for (const source of processed.sources) {\n const fileName = join(outSubDir, basename(source.src))\n const content = await readFile(source.src)\n ctx.emitFile({ type: 'asset', fileName, source: content })\n source.src = `/${fileName}`\n }\n}\n\nfunction rebuildFormatSrcsets(processed: ProcessedImage, fallbackPath: string) {\n const formatGroups = new Map<string, string[]>()\n for (const s of processed.sources) {\n let group = formatGroups.get(s.format)\n if (!group) {\n group = []\n formatGroups.set(s.format, group)\n }\n group.push(`${s.src} ${s.width}w`)\n }\n processed.formats = [...formatGroups.entries()].map(([fmt, entries]) => ({\n type: `image/${fmt}`,\n srcset: entries.join(', '),\n }))\n\n const lastFormat = processed.formats.at(-1)\n processed.srcset = lastFormat?.srcset ?? ''\n processed.src = processed.sources.at(-1)?.src ?? fallbackPath\n}\n\n// ─── Image processing utilities ─────────────────────────────────────────────\n\ninterface ProcessOptions {\n widths: number[]\n formats: ImageFormat[]\n quality: number\n placeholderSize: number\n outSubDir: string\n outDir: string\n}\n\nasync function processImage(absPath: string, opts: ProcessOptions): Promise<ProcessedImage> {\n const metadata = await getImageMetadata(absPath)\n const ext = extname(absPath)\n const name = basename(absPath, ext)\n const sources: Array<{ src: string; width: number; format: string }> = []\n\n // Ensure output directory exists\n const processedDir = join(opts.outDir, opts.outSubDir)\n if (!existsSync(processedDir)) {\n await mkdir(processedDir, { recursive: true })\n }\n\n // Generate resized variants — iterate formats first so sources are grouped by format\n for (const format of opts.formats) {\n for (const targetWidth of opts.widths) {\n // Don't upscale\n const width = Math.min(targetWidth, metadata.width)\n const outName = `${name}-${width}.${format}`\n const outPath = join(processedDir, outName)\n\n await resizeImage(absPath, outPath, width, format, opts.quality)\n sources.push({ src: outPath, width, format })\n }\n }\n\n // Build per-format source sets for <picture>\n const formatGroups = new Map<string, Array<{ src: string; width: number }>>()\n for (const s of sources) {\n let group = formatGroups.get(s.format)\n if (!group) {\n group = []\n formatGroups.set(s.format, group)\n }\n group.push({ src: s.src, width: s.width })\n }\n\n const formats: FormatSource[] = [...formatGroups.entries()].map(([fmt, group]) => ({\n type: `image/${fmt === 'jpeg' ? 'jpeg' : fmt}`,\n srcset: group.map((s) => `${s.src} ${s.width}w`).join(', '),\n }))\n\n // Fallback: last format's srcset\n const fallbackFormat = formats[formats.length - 1]\n const fallbackSources = formatGroups.get([...formatGroups.keys()].pop()!)!\n\n // Generate blur placeholder\n const placeholder = await generateBlurPlaceholder(absPath, opts.placeholderSize)\n\n return {\n src: fallbackSources[fallbackSources.length - 1]?.src ?? absPath,\n srcset: fallbackFormat?.srcset ?? '',\n width: metadata.width,\n height: metadata.height,\n placeholder,\n formats,\n sources,\n }\n}\n\ninterface ImageMetadata {\n width: number\n height: number\n format: string\n}\n\n/**\n * Read basic image metadata.\n * Uses minimal binary header parsing — no external dependencies.\n */\nasync function getImageMetadata(absPath: string): Promise<ImageMetadata> {\n const buffer = await readFile(absPath)\n const ext = extname(absPath).toLowerCase()\n\n if (ext === '.png') {\n // PNG: width at bytes 16-19, height at 20-23 (big-endian)\n const width = buffer.readUInt32BE(16)\n const height = buffer.readUInt32BE(20)\n return { width, height, format: 'png' }\n }\n\n if (ext === '.jpg' || ext === '.jpeg') {\n // JPEG: scan for SOF markers\n const dimensions = parseJpegDimensions(buffer)\n return { ...dimensions, format: 'jpeg' }\n }\n\n if (ext === '.webp') {\n // WebP: VP8 header\n const dimensions = parseWebPDimensions(buffer)\n return { ...dimensions, format: 'webp' }\n }\n\n // Fallback\n return { width: 0, height: 0, format: ext.slice(1) }\n}\n\n/** @internal Exported for testing */\nexport function parseJpegDimensions(buffer: Buffer): {\n width: number\n height: number\n} {\n let offset = 2 // Skip SOI marker\n while (offset < buffer.length) {\n if (buffer[offset] !== 0xff) break\n const marker = buffer[offset + 1]!\n // SOF markers (0xC0-0xCF except 0xC4, 0xC8, 0xCC)\n if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {\n const height = buffer.readUInt16BE(offset + 5)\n const width = buffer.readUInt16BE(offset + 7)\n return { width, height }\n }\n const length = buffer.readUInt16BE(offset + 2)\n offset += 2 + length\n }\n return { width: 0, height: 0 }\n}\n\n/** @internal Exported for testing */\nexport function parseWebPDimensions(buffer: Buffer): {\n width: number\n height: number\n} {\n // RIFF header: bytes 0-3 = \"RIFF\", 8-11 = \"WEBP\"\n const chunk = buffer.toString('ascii', 12, 16)\n if (chunk === 'VP8 ') {\n // Lossy VP8\n const width = buffer.readUInt16LE(26) & 0x3fff\n const height = buffer.readUInt16LE(28) & 0x3fff\n return { width, height }\n }\n if (chunk === 'VP8L') {\n // Lossless VP8L\n const bits = buffer.readUInt32LE(21)\n const width = (bits & 0x3fff) + 1\n const height = ((bits >> 14) & 0x3fff) + 1\n return { width, height }\n }\n if (chunk === 'VP8X') {\n // Extended VP8X\n const width = 1 + ((buffer[24]! | (buffer[25]! << 8) | (buffer[26]! << 16)) & 0xffffff)\n const height = 1 + ((buffer[27]! | (buffer[28]! << 8) | (buffer[29]! << 16)) & 0xffffff)\n return { width, height }\n }\n return { width: 0, height: 0 }\n}\n\n/**\n * Resize an image using native platform capabilities.\n * Uses sharp if available, falls back to canvas API.\n */\nasync function resizeImage(\n input: string,\n output: string,\n width: number,\n format: ImageFormat,\n quality: number,\n): Promise<void> {\n try {\n // Try sharp (the standard Node.js image processing library)\n const sharp = await import('sharp').then((m) => m.default ?? m)\n let pipeline = sharp(input).resize(width)\n\n switch (format) {\n case 'webp':\n pipeline = pipeline.webp({ quality })\n break\n case 'avif':\n pipeline = pipeline.avif({ quality })\n break\n case 'jpeg':\n pipeline = pipeline.jpeg({ quality, mozjpeg: true })\n break\n case 'png':\n pipeline = pipeline.png({ compressionLevel: 9 })\n break\n }\n\n await pipeline.toFile(output)\n } catch {\n // sharp not available — copy original as fallback\n warnSharpMissing()\n const content = await readFile(input)\n await writeFile(output, content)\n }\n}\n\n/**\n * Generate a tiny blur placeholder as a base64 data URI.\n */\nasync function generateBlurPlaceholder(input: string, size: number): Promise<string> {\n try {\n const sharp = await import('sharp').then((m) => m.default ?? m)\n const buffer = await sharp(input)\n .resize(size, size, { fit: 'inside' })\n .blur(2)\n .webp({ quality: 20 })\n .toBuffer()\n\n return `data:image/webp;base64,${buffer.toString('base64')}`\n } catch {\n // sharp not available — return a transparent placeholder\n return \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='1'%3E%3C/svg%3E\"\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"image-plugin.js","names":[],"sources":["../src/image-plugin.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { basename, extname, join } from 'node:path'\nimport type { Plugin } from 'vite'\n\nlet sharpWarned = false\nfunction warnSharpMissing() {\n if (sharpWarned) return\n sharpWarned = true\n // oxlint-disable-next-line no-console\n console.warn(\n '\\n[Pyreon] sharp not installed — images will not be optimized. Install for full support: bun add -D sharp\\n',\n )\n}\n\n// ─── Image processing Vite plugin ──────────────────────────────────────────\n//\n// Processes images at build time:\n// - Generates multiple sizes for responsive srcset\n// - Converts to modern formats (WebP, AVIF)\n// - Creates tiny blur placeholders (base64 inline)\n// - Outputs optimized images to the build directory\n//\n// Usage in code:\n// import heroImg from \"./hero.jpg?optimize\"\n// // → { src, srcset, width, height, placeholder }\n//\n// Or use the component helper:\n// import { Image } from \"@pyreon/zero/image\"\n// <Image src=\"/hero.jpg\" width={1920} height={1080} optimize />\n\n/**\n * CDN provider — rewrites image URLs to CDN endpoints.\n * Return the rewritten URL, or null to use local processing.\n */\nexport type ImageCdnProvider = (src: string, opts: {\n width: number\n quality: number\n format: ImageFormat\n}) => string | null\n\n/** Built-in CDN providers. */\nexport const cdnProviders = {\n /** Cloudinary: `https://res.cloudinary.com/{cloud}/image/upload/...` */\n cloudinary: (cloudName: string): ImageCdnProvider => (src, { width, quality, format }) =>\n `https://res.cloudinary.com/${cloudName}/image/upload/w_${width},q_${quality},f_${format}/${src}`,\n\n /** Imgix: `https://{domain}.imgix.net/...?w=...&q=...&fm=...` */\n imgix: (domain: string): ImageCdnProvider => (src, { width, quality, format }) =>\n `https://${domain}.imgix.net/${src}?w=${width}&q=${quality}&fm=${format}&auto=format`,\n\n /** Vercel Image Optimization: `/_next/image?url=...&w=...&q=...` */\n vercel: (): ImageCdnProvider => (src, { width, quality }) =>\n `/_vercel/image?url=${encodeURIComponent(src)}&w=${width}&q=${quality}`,\n\n /** Bunny CDN: `https://{pullZone}.b-cdn.net/...?width=...&quality=...` */\n bunny: (pullZone: string): ImageCdnProvider => (src, { width, quality }) =>\n `https://${pullZone}.b-cdn.net/${src}?width=${width}&quality=${quality}`,\n} as const\n\n/** Placeholder generation strategy. */\nexport type PlaceholderStrategy = 'blur' | 'dominant-color' | 'none'\n\n/** SVG processing options for ?component imports. */\nexport interface SvgOptions {\n /** Replace fill/stroke with currentColor. Default: true */\n currentColor?: boolean\n /** Default size (width/height). */\n defaultSize?: number\n}\n\nexport interface ImagePluginConfig {\n /** Output directory for processed images. Default: \"assets/img\" */\n outDir?: string\n /** Default widths for responsive images. Default: [640, 1024, 1920] */\n widths?: number[]\n /** Output formats. Default: [\"webp\"] */\n formats?: ImageFormat[]\n /** Quality for lossy formats (1-100). Default: 80 */\n quality?: number\n /** Blur placeholder size in px. Default: 16 */\n placeholderSize?: number\n /** Placeholder strategy. Default: \"blur\" */\n placeholder?: PlaceholderStrategy\n /** File patterns to process. Default: /\\.(jpe?g|png|webp|avif)$/i */\n include?: RegExp\n /**\n * CDN provider for URL rewriting. When set, images are NOT processed\n * locally — URLs are rewritten to the CDN endpoint.\n *\n * @example\n * ```ts\n * imagePlugin({ cdn: cdnProviders.cloudinary('my-cloud') })\n * imagePlugin({ cdn: cdnProviders.vercel() })\n * ```\n */\n cdn?: ImageCdnProvider\n /**\n * SVG processing options. Enables `?component` import for inline SVGs.\n *\n * @example\n * ```tsx\n * import Logo from './logo.svg?component'\n * <Logo width={24} class=\"text-primary\" />\n * ```\n */\n svg?: SvgOptions | boolean\n}\n\nexport type ImageFormat = 'webp' | 'avif' | 'jpeg' | 'png'\n\n/** Per-format source set for <picture> <source> elements. */\nexport interface FormatSource {\n /** MIME type. e.g. \"image/webp\", \"image/avif\" */\n type: string\n /** srcset string for this format. e.g. \"/img-640.webp 640w, /img-1920.webp 1920w\" */\n srcset: string\n}\n\nexport interface ProcessedImage {\n /** Fallback source path (last format, largest width). */\n src: string\n /** Fallback srcset string (last format). */\n srcset: string\n /** Intrinsic width. */\n width: number\n /** Intrinsic height. */\n height: number\n /** Base64 blur placeholder data URI. */\n placeholder: string\n /** Per-format source sets for <picture> element. Ordered by priority (best format first). */\n formats: FormatSource[]\n /** Flat list of all sources. */\n sources: Array<{ src: string; width: number; format: string }>\n}\n\nconst IMAGE_EXT_RE = /\\.(jpe?g|png|webp|avif)$/i\n\n/**\n * Zero image processing Vite plugin.\n *\n * Transforms image imports with query params into optimized responsive images:\n *\n * @example\n * // vite.config.ts\n * import { imagePlugin } from \"@pyreon/zero/image-plugin\"\n *\n * export default {\n * plugins: [\n * pyreon(),\n * zero(),\n * imagePlugin({ widths: [480, 960, 1440], quality: 85 }),\n * ],\n * }\n *\n * @example\n * // In a component — import with ?optimize\n * import hero from \"./images/hero.jpg?optimize\"\n * // hero = { src, srcset, width, height, placeholder }\n *\n * <Image {...hero} alt=\"Hero\" priority />\n */\nexport function imagePlugin(config: ImagePluginConfig = {}): Plugin {\n const defaultWidths = config.widths ?? [640, 1024, 1920]\n const defaultFormats = config.formats ?? ['webp']\n const quality = config.quality ?? 80\n const placeholderSize = config.placeholderSize ?? 16\n const placeholderStrategy = config.placeholder ?? 'blur'\n const outSubDir = config.outDir ?? 'assets/img'\n const include = config.include ?? IMAGE_EXT_RE\n const cdn = config.cdn\n const svgOpts: SvgOptions | false = config.svg === true\n ? { currentColor: true }\n : config.svg === false || config.svg === undefined\n ? false\n : config.svg\n\n let root = ''\n let outDir = ''\n let isBuild = false\n\n return {\n name: 'pyreon-zero-images',\n enforce: 'pre',\n\n configResolved(resolvedConfig) {\n root = resolvedConfig.root\n outDir = resolvedConfig.build.outDir\n isBuild = resolvedConfig.command === 'build'\n },\n\n async resolveId(id) {\n // SVG as component: import Logo from './logo.svg?component'\n if (svgOpts && id.includes('?component') && id.split('?')[0]!.endsWith('.svg')) {\n return `\\0virtual:zero-svg:${id}`\n }\n // Handle ?optimize query on image imports\n if (id.includes('?optimize') && include.test(id.split('?')[0]!)) {\n return `\\0virtual:zero-image:${id}`\n }\n return null\n },\n\n async load(id) {\n // SVG component loading\n if (id.startsWith('\\0virtual:zero-svg:')) {\n const rawPath = id.replace('\\0virtual:zero-svg:', '').split('?')[0] ?? id\n const absPath = rawPath.startsWith('/') ? join(root, rawPath) : rawPath\n if (!existsSync(absPath)) return null\n\n let svg = await readFile(absPath, 'utf-8')\n\n // Replace fill/stroke with currentColor\n if (svgOpts && (svgOpts as SvgOptions).currentColor !== false) {\n svg = svg\n .replace(/fill=\"(?!none)[^\"]*\"/g, 'fill=\"currentColor\"')\n .replace(/stroke=\"(?!none)[^\"]*\"/g, 'stroke=\"currentColor\"')\n }\n\n // Add default size from config\n const defaultSize = svgOpts && (svgOpts as SvgOptions).defaultSize\n if (defaultSize && !svg.includes('width=')) {\n svg = svg.replace('<svg', `<svg width=\"${defaultSize}\" height=\"${defaultSize}\"`)\n }\n\n // Export as Pyreon component\n return `\nimport { h } from '@pyreon/core'\nconst _svg = ${JSON.stringify(svg)}\nexport default function SvgComponent(props) {\n const el = h('span', {\n ...props,\n dangerouslySetInnerHTML: { __html: _svg },\n style: [\n 'display:inline-flex;align-items:center;justify-content:center',\n props.width ? 'width:' + props.width + 'px' : '',\n props.height ? 'height:' + props.height + 'px' : '',\n props.style || '',\n ].filter(Boolean).join(';'),\n })\n return el\n}\n`\n }\n\n // Image optimization loading\n if (!id.startsWith('\\0virtual:zero-image:')) return null\n\n const rawPath = id.replace('\\0virtual:zero-image:', '').split('?')[0] ?? id\n const absPath = rawPath.startsWith('/') ? join(root, 'public', rawPath) : rawPath\n\n // CDN mode — rewrite URLs, no local processing\n if (cdn) {\n const metadata = await getImageMetadata(absPath)\n const sources = defaultWidths.map((w) => ({\n src: cdn(rawPath, { width: w, quality, format: defaultFormats[0]! }) ?? rawPath,\n width: w,\n format: defaultFormats[0]! as string,\n }))\n const srcset = sources.map((s) => `${s.src} ${s.width}w`).join(', ')\n const result: ProcessedImage = {\n src: sources[sources.length - 1]?.src ?? rawPath,\n srcset,\n width: metadata.width,\n height: metadata.height,\n placeholder: placeholderStrategy === 'none' ? ''\n : await generateBlurPlaceholder(absPath, placeholderSize),\n formats: defaultFormats.map((fmt) => ({\n type: `image/${fmt}`,\n srcset: defaultWidths\n .map((w) => `${cdn(rawPath, { width: w, quality, format: fmt }) ?? rawPath} ${w}w`)\n .join(', '),\n })),\n sources,\n }\n return `export default ${JSON.stringify(result)}`\n }\n\n if (!isBuild) {\n const result = await loadDevImage(absPath, rawPath, placeholderSize)\n return `export default ${JSON.stringify(result)}`\n }\n\n const processed = await processImage(absPath, {\n widths: defaultWidths,\n formats: defaultFormats,\n quality,\n placeholderSize,\n outSubDir,\n outDir: join(root, outDir),\n })\n\n await emitProcessedSources(processed, outSubDir, this)\n rebuildFormatSrcsets(processed, absPath)\n\n return `export default ${JSON.stringify(processed)}`\n },\n }\n}\n\nasync function loadDevImage(\n absPath: string,\n rawPath: string,\n placeholderSize: number,\n): Promise<ProcessedImage> {\n const metadata = await getImageMetadata(absPath)\n const publicPath = rawPath.startsWith('/') ? rawPath : `/@fs/${absPath}`\n\n return {\n src: publicPath,\n srcset: '',\n width: metadata.width,\n height: metadata.height,\n placeholder: await generateBlurPlaceholder(absPath, placeholderSize),\n formats: [],\n sources: [{ src: publicPath, width: metadata.width, format: 'original' }],\n }\n}\n\nasync function emitProcessedSources(\n processed: ProcessedImage,\n outSubDir: string,\n ctx: {\n emitFile: (f: { type: 'asset'; fileName: string; source: Uint8Array }) => void\n },\n) {\n for (const source of processed.sources) {\n const fileName = join(outSubDir, basename(source.src))\n const content = await readFile(source.src)\n ctx.emitFile({ type: 'asset', fileName, source: content })\n source.src = `/${fileName}`\n }\n}\n\nfunction rebuildFormatSrcsets(processed: ProcessedImage, fallbackPath: string) {\n const formatGroups = new Map<string, string[]>()\n for (const s of processed.sources) {\n let group = formatGroups.get(s.format)\n if (!group) {\n group = []\n formatGroups.set(s.format, group)\n }\n group.push(`${s.src} ${s.width}w`)\n }\n processed.formats = [...formatGroups.entries()].map(([fmt, entries]) => ({\n type: `image/${fmt}`,\n srcset: entries.join(', '),\n }))\n\n const lastFormat = processed.formats.at(-1)\n processed.srcset = lastFormat?.srcset ?? ''\n processed.src = processed.sources.at(-1)?.src ?? fallbackPath\n}\n\n// ─── Image processing utilities ─────────────────────────────────────────────\n\ninterface ProcessOptions {\n widths: number[]\n formats: ImageFormat[]\n quality: number\n placeholderSize: number\n outSubDir: string\n outDir: string\n}\n\nasync function processImage(absPath: string, opts: ProcessOptions): Promise<ProcessedImage> {\n const metadata = await getImageMetadata(absPath)\n const ext = extname(absPath)\n const name = basename(absPath, ext)\n const sources: Array<{ src: string; width: number; format: string }> = []\n\n // Ensure output directory exists\n const processedDir = join(opts.outDir, opts.outSubDir)\n if (!existsSync(processedDir)) {\n await mkdir(processedDir, { recursive: true })\n }\n\n // Generate resized variants — iterate formats first so sources are grouped by format\n for (const format of opts.formats) {\n for (const targetWidth of opts.widths) {\n // Don't upscale\n const width = Math.min(targetWidth, metadata.width)\n const outName = `${name}-${width}.${format}`\n const outPath = join(processedDir, outName)\n\n await resizeImage(absPath, outPath, width, format, opts.quality)\n sources.push({ src: outPath, width, format })\n }\n }\n\n // Build per-format source sets for <picture>\n const formatGroups = new Map<string, Array<{ src: string; width: number }>>()\n for (const s of sources) {\n let group = formatGroups.get(s.format)\n if (!group) {\n group = []\n formatGroups.set(s.format, group)\n }\n group.push({ src: s.src, width: s.width })\n }\n\n const formats: FormatSource[] = [...formatGroups.entries()].map(([fmt, group]) => ({\n type: `image/${fmt === 'jpeg' ? 'jpeg' : fmt}`,\n srcset: group.map((s) => `${s.src} ${s.width}w`).join(', '),\n }))\n\n // Fallback: last format's srcset\n const fallbackFormat = formats[formats.length - 1]\n const fallbackSources = formatGroups.get([...formatGroups.keys()].pop()!)!\n\n // Generate blur placeholder\n const placeholder = await generateBlurPlaceholder(absPath, opts.placeholderSize)\n\n return {\n src: fallbackSources[fallbackSources.length - 1]?.src ?? absPath,\n srcset: fallbackFormat?.srcset ?? '',\n width: metadata.width,\n height: metadata.height,\n placeholder,\n formats,\n sources,\n }\n}\n\ninterface ImageMetadata {\n width: number\n height: number\n format: string\n}\n\n/**\n * Read basic image metadata.\n * Uses minimal binary header parsing — no external dependencies.\n */\nasync function getImageMetadata(absPath: string): Promise<ImageMetadata> {\n const buffer = await readFile(absPath)\n const ext = extname(absPath).toLowerCase()\n\n if (ext === '.png') {\n // PNG: width at bytes 16-19, height at 20-23 (big-endian)\n const width = buffer.readUInt32BE(16)\n const height = buffer.readUInt32BE(20)\n return { width, height, format: 'png' }\n }\n\n if (ext === '.jpg' || ext === '.jpeg') {\n // JPEG: scan for SOF markers\n const dimensions = parseJpegDimensions(buffer)\n return { ...dimensions, format: 'jpeg' }\n }\n\n if (ext === '.webp') {\n // WebP: VP8 header\n const dimensions = parseWebPDimensions(buffer)\n return { ...dimensions, format: 'webp' }\n }\n\n // Fallback\n return { width: 0, height: 0, format: ext.slice(1) }\n}\n\n/** @internal Exported for testing */\nexport function parseJpegDimensions(buffer: Buffer): {\n width: number\n height: number\n} {\n let offset = 2 // Skip SOI marker\n while (offset < buffer.length) {\n if (buffer[offset] !== 0xff) break\n const marker = buffer[offset + 1]!\n // SOF markers (0xC0-0xCF except 0xC4, 0xC8, 0xCC)\n if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {\n const height = buffer.readUInt16BE(offset + 5)\n const width = buffer.readUInt16BE(offset + 7)\n return { width, height }\n }\n const length = buffer.readUInt16BE(offset + 2)\n offset += 2 + length\n }\n return { width: 0, height: 0 }\n}\n\n/** @internal Exported for testing */\nexport function parseWebPDimensions(buffer: Buffer): {\n width: number\n height: number\n} {\n // RIFF header: bytes 0-3 = \"RIFF\", 8-11 = \"WEBP\"\n const chunk = buffer.toString('ascii', 12, 16)\n if (chunk === 'VP8 ') {\n // Lossy VP8\n const width = buffer.readUInt16LE(26) & 0x3fff\n const height = buffer.readUInt16LE(28) & 0x3fff\n return { width, height }\n }\n if (chunk === 'VP8L') {\n // Lossless VP8L\n const bits = buffer.readUInt32LE(21)\n const width = (bits & 0x3fff) + 1\n const height = ((bits >> 14) & 0x3fff) + 1\n return { width, height }\n }\n if (chunk === 'VP8X') {\n // Extended VP8X\n const width = 1 + ((buffer[24]! | (buffer[25]! << 8) | (buffer[26]! << 16)) & 0xffffff)\n const height = 1 + ((buffer[27]! | (buffer[28]! << 8) | (buffer[29]! << 16)) & 0xffffff)\n return { width, height }\n }\n return { width: 0, height: 0 }\n}\n\n/**\n * Resize an image using native platform capabilities.\n * Uses sharp if available, falls back to canvas API.\n */\nasync function resizeImage(\n input: string,\n output: string,\n width: number,\n format: ImageFormat,\n quality: number,\n): Promise<void> {\n try {\n // Try sharp (the standard Node.js image processing library)\n const sharp = await import('sharp').then((m) => m.default ?? m)\n let pipeline = sharp(input).resize(width)\n\n switch (format) {\n case 'webp':\n pipeline = pipeline.webp({ quality })\n break\n case 'avif':\n pipeline = pipeline.avif({ quality })\n break\n case 'jpeg':\n pipeline = pipeline.jpeg({ quality, mozjpeg: true })\n break\n case 'png':\n pipeline = pipeline.png({ compressionLevel: 9 })\n break\n }\n\n await pipeline.toFile(output)\n } catch {\n // sharp not available — copy original as fallback\n warnSharpMissing()\n const content = await readFile(input)\n await writeFile(output, content)\n }\n}\n\n/**\n * Generate a tiny blur placeholder as a base64 data URI.\n */\nasync function generateBlurPlaceholder(input: string, size: number): Promise<string> {\n try {\n const sharp = await import('sharp').then((m) => m.default ?? m)\n const buffer = await sharp(input)\n .resize(size, size, { fit: 'inside' })\n .blur(2)\n .webp({ quality: 20 })\n .toBuffer()\n\n return `data:image/webp;base64,${buffer.toString('base64')}`\n } catch {\n // sharp not available — return a transparent placeholder\n return \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='1'%3E%3C/svg%3E\"\n }\n}\n"],"mappings":"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"}
package/lib/index.js CHANGED
@@ -175,6 +175,7 @@ function Image(props) {
175
175
  const MAX_PREFETCH_CACHE = 200;
176
176
  const prefetched = /* @__PURE__ */ new Set();
177
177
  function doPrefetch(href) {
178
+ if (typeof document === "undefined") return;
178
179
  if (prefetched.has(href)) return;
179
180
  if (prefetched.size >= MAX_PREFETCH_CACHE) {
180
181
  const first = prefetched.values().next().value;
@@ -363,6 +364,7 @@ const Link = createLink((props) => /* @__PURE__ */ jsx("a", {
363
364
  */
364
365
  function Script(props) {
365
366
  function loadScript() {
367
+ if (typeof document === "undefined") return;
366
368
  if (props.id && document.getElementById(props.id)) return;
367
369
  const script = document.createElement("script");
368
370
  if (props.src) script.src = props.src;
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../../../core/core/lib/jsx-runtime.js","../src/image.tsx","../src/link.tsx","../src/script.tsx","../src/i18n-routing.ts","../src/meta.tsx","../src/theme.tsx","../src/index.ts"],"sourcesContent":["import { onMount, onUnmount } from '@pyreon/core'\n\n/**\n * Observes an element and calls `onIntersect` once it enters the viewport.\n * Automatically disconnects after the first intersection.\n *\n * @param getElement - Getter for the target element (may be undefined before mount).\n * @param onIntersect - Callback fired when the element becomes visible.\n * @param rootMargin - IntersectionObserver rootMargin. Default: \"200px\".\n */\nexport function useIntersectionObserver(\n getElement: () => HTMLElement | undefined,\n onIntersect: () => void,\n rootMargin = '200px',\n) {\n onMount(() => {\n const el = getElement()\n if (!el) return undefined\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n onIntersect()\n observer.disconnect()\n }\n }\n },\n { rootMargin },\n )\n\n observer.observe(el)\n onUnmount(() => observer.disconnect())\n return undefined\n })\n}\n","//#region src/h.ts\n/** Marker for fragment nodes — renders children without a wrapper element */\nconst Fragment = Symbol(\"Pyreon.Fragment\");\n/**\n* Hyperscript function — the compiled output of JSX.\n* `<div class=\"x\">hello</div>` → `h(\"div\", { class: \"x\" }, \"hello\")`\n*\n* Generic on P so TypeScript validates props match the component's signature\n* at the call site, then stores the result in the loosely-typed VNode.\n*/\n/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */\nconst EMPTY_PROPS = {};\nfunction h(type, props, ...children) {\n\treturn {\n\t\ttype,\n\t\tprops: props ?? EMPTY_PROPS,\n\t\tchildren: normalizeChildren(children),\n\t\tkey: props?.key ?? null\n\t};\n}\nfunction normalizeChildren(children) {\n\tfor (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);\n\treturn children;\n}\nfunction flattenChildren(children) {\n\tconst result = [];\n\tfor (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));\n\telse result.push(child);\n\treturn result;\n}\n\n//#endregion\n//#region src/jsx-runtime.ts\n/**\n* JSX automatic runtime.\n*\n* When tsconfig has `\"jsxImportSource\": \"@pyreon/core\"`, the TS/bundler compiler\n* rewrites JSX to imports from this file automatically:\n* <div class=\"x\" /> → jsx(\"div\", { class: \"x\" })\n*/\nfunction jsx(type, props, key) {\n\tconst { children, ...rest } = props;\n\tconst propsWithKey = key != null ? {\n\t\t...rest,\n\t\tkey\n\t} : rest;\n\tif (typeof type === \"function\") return h(type, children !== void 0 ? {\n\t\t...propsWithKey,\n\t\tchildren\n\t} : propsWithKey);\n\treturn h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);\n}\nconst jsxs = jsx;\n\n//#endregion\nexport { Fragment, jsx, jsxs };\n//# sourceMappingURL=jsx-runtime.js.map","import type { VNodeChild } from '@pyreon/core'\nimport { createRef } from '@pyreon/core'\nimport { signal } from '@pyreon/reactivity'\nimport type { FormatSource } from './image-plugin'\nimport { useIntersectionObserver } from './utils/use-intersection-observer'\n\n// ─── Image optimization component ───────────────────────────────────────────\n//\n// <Image> provides:\n// - Lazy loading via IntersectionObserver (loads when near viewport)\n// - Automatic width/height to prevent CLS (Cumulative Layout Shift)\n// - Responsive srcset generation from width descriptors\n// - Multi-format support via <picture> (WebP/AVIF with fallback)\n// - Blur-up placeholder while loading\n// - Priority loading for above-the-fold images\n\nexport interface ImageProps {\n /** Image source URL. */\n src: string\n /** Alt text (required for accessibility). */\n alt: string\n /** Intrinsic width of the image. */\n width: number\n /** Intrinsic height of the image. */\n height: number\n /** Responsive sizes attribute. Default: \"100vw\" */\n sizes?: string\n /** Responsive srcset string or source array. */\n srcset?: string | ImageSource[]\n /** Per-format source sets for <picture>. Provided automatically by imagePlugin. */\n formats?: FormatSource[]\n /** Loading strategy. \"lazy\" uses IntersectionObserver, \"eager\" loads immediately. Default: \"lazy\" */\n loading?: 'lazy' | 'eager'\n /** Mark as priority (LCP image). Disables lazy loading, adds fetchPriority=\"high\". */\n priority?: boolean\n /** Low-quality placeholder image URL or base64 data URI for blur-up effect. */\n placeholder?: string\n /** CSS class name. */\n class?: string\n /** Inline styles. */\n style?: string\n /** CSS object-fit. Default: \"cover\" */\n fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'\n /** Decode async. Default: true */\n decoding?: 'sync' | 'async' | 'auto'\n /**\n * Raw mode — renders a plain `<img>` without the container div,\n * aspect-ratio, max-width, or lazy loading wrapper.\n * Use when the Image is inside a custom layout (absolute positioning, etc.).\n */\n raw?: boolean\n}\n\nexport interface ImageSource {\n src: string\n width: number\n}\n\n/**\n * Optimized image component with lazy loading, responsive images,\n * multi-format <picture> support, and blur-up placeholders.\n *\n * @example\n * // With imagePlugin — spread the import directly\n * import hero from \"./hero.jpg?optimize\"\n * <Image {...hero} alt=\"Hero\" priority />\n *\n * @example\n * // Manual usage\n * <Image src=\"/hero.jpg\" alt=\"Hero\" width={1200} height={630} />\n */\nexport function Image(props: ImageProps): VNodeChild {\n // Raw mode: plain <img> without container, lazy loading, or layout constraints\n if (props.raw) {\n return (\n <img\n src={props.src}\n alt={props.alt}\n width={props.width}\n height={props.height}\n class={props.class}\n style={props.style}\n decoding={props.decoding ?? 'async'}\n loading={props.loading ?? 'lazy'}\n fetchPriority={props.priority ? 'high' : undefined}\n />\n ) as any\n }\n\n const isEager = props.priority || props.loading === 'eager'\n const loaded = signal(isEager)\n const inView = signal(isEager)\n const containerRef = createRef<HTMLElement>()\n\n // Resolve srcset from string or array\n const resolvedSrcset =\n typeof props.srcset === 'string'\n ? props.srcset\n : props.srcset?.map((s) => `${s.src} ${s.width}w`).join(', ')\n\n const sizes = props.sizes ?? '100vw'\n const fit = props.fit ?? 'cover'\n const hasFormats = props.formats && props.formats.length > 0\n const aspectRatio = `${props.width} / ${props.height}`\n\n if (!isEager) {\n useIntersectionObserver(\n () => containerRef.current ?? undefined,\n () => inView.set(true),\n )\n }\n\n // Static styles (don't depend on signals)\n const containerStyle = [\n 'position: relative',\n 'overflow: hidden',\n `aspect-ratio: ${aspectRatio}`,\n `max-width: ${props.width}px`,\n 'width: 100%',\n props.style,\n ]\n .filter(Boolean)\n .join('; ')\n\n const imgEl = (\n <img\n src={() => (inView() ? props.src : '')}\n srcSet={() => (!hasFormats && inView() && resolvedSrcset ? resolvedSrcset : '')}\n sizes={resolvedSrcset ? sizes : undefined}\n alt={props.alt}\n width={props.width}\n height={props.height}\n loading={isEager ? 'eager' : 'lazy'}\n decoding={props.decoding ?? 'async'}\n fetchPriority={props.priority ? 'high' : undefined}\n onLoad={() => loaded.set(true)}\n style={() =>\n [\n 'display: block',\n 'width: 100%',\n 'height: 100%',\n `object-fit: ${fit}`,\n 'transition: opacity 0.3s ease',\n props.placeholder && !loaded() ? 'opacity: 0' : 'opacity: 1',\n ].join('; ')\n }\n />\n )\n\n return (\n <div ref={containerRef} class={props.class} style={containerStyle}>\n {props.placeholder && (\n <img\n src={props.placeholder}\n alt=\"\"\n aria-hidden=\"true\"\n loading=\"eager\"\n style={() =>\n [\n 'position: absolute',\n 'inset: 0',\n 'width: 100%',\n 'height: 100%',\n 'object-fit: cover',\n 'filter: blur(20px)',\n 'transform: scale(1.1)',\n 'transition: opacity 0.4s ease',\n loaded() ? 'opacity: 0; pointer-events: none' : 'opacity: 1',\n ].join('; ')\n }\n />\n )}\n {hasFormats ? (\n <picture>\n {props.formats?.map((fmt) => (\n <source\n type={fmt.type}\n srcSet={() => (inView() ? (fmt.srcset ?? '') : '')}\n sizes={sizes}\n />\n ))}\n {imgEl}\n </picture>\n ) : (\n imgEl\n )}\n </div>\n )\n}\n","import { createRef } from '@pyreon/core'\nimport { useRouter } from '@pyreon/router'\nimport { useIntersectionObserver } from './utils/use-intersection-observer'\n\n// ─── Link component with prefetching ────────────────────────────────────────\n//\n// Provides client-side navigation, prefetching, and active state tracking.\n// Three levels of API:\n//\n// 1. useLink(props) — composable returning handlers, state, and ref callback\n// 2. createLink(Comp) — HOC wrapping any component with link behavior\n// 3. Link — default <a>-based link (built on createLink)\n\nexport interface LinkProps {\n /** Target URL path. */\n href: string\n /** Link content. */\n children?: any\n /** CSS class name. */\n class?: string\n /** Class applied when this link matches the current route. */\n activeClass?: string\n /** Class applied when this link exactly matches the current route. */\n exactActiveClass?: string\n /** Prefetch strategy. Default: \"hover\" */\n prefetch?: 'hover' | 'viewport' | 'none'\n /** Open in new tab. */\n external?: boolean\n /** Inline styles. */\n style?: string\n /** ARIA label. */\n 'aria-label'?: string\n /** Additional click handler — called before navigation. Call e.preventDefault() to cancel. */\n onClick?: ((e: MouseEvent) => void) | undefined\n}\n\n/** Props passed to a custom component via createLink. */\nexport interface LinkRenderProps {\n href: string\n ref: import('@pyreon/core').Ref<HTMLAnchorElement>\n onClick: (e: MouseEvent) => void\n onMouseEnter: () => void\n onTouchStart: () => void\n isActive: () => boolean\n isExactActive: () => boolean\n /** Reactive class string — pass directly to element for auto-updates on route change. */\n class: (() => string) | string | undefined\n style?: string\n target?: string\n rel?: string\n 'aria-label'?: string\n children?: any\n}\n\n/** Return type of useLink. */\nexport interface UseLinkReturn {\n /** Ref object — attach to the root element for viewport-based prefetch. */\n ref: import('@pyreon/core').Ref<HTMLAnchorElement>\n /** Click handler — performs client-side navigation. */\n handleClick: (e: MouseEvent) => void\n /** Mouse enter handler — triggers hover prefetch. */\n handleMouseEnter: () => void\n /** Touch start handler — triggers prefetch on mobile. */\n handleTouchStart: () => void\n /** Whether the link partially matches the current route. */\n isActive: () => boolean\n /** Whether the link exactly matches the current route. */\n isExactActive: () => boolean\n /** Resolved class string including active classes. */\n classes: () => string\n}\n\nconst MAX_PREFETCH_CACHE = 200\nconst prefetched = new Set<string>()\n\nfunction doPrefetch(href: string) {\n if (prefetched.has(href)) return\n // Evict oldest entries when cache is full\n if (prefetched.size >= MAX_PREFETCH_CACHE) {\n const first = prefetched.values().next().value\n if (first) prefetched.delete(first)\n }\n prefetched.add(href)\n\n const docLink = document.createElement('link')\n docLink.rel = 'prefetch'\n docLink.href = href\n docLink.as = 'document'\n document.head.appendChild(docLink)\n\n try {\n const chunkHint = document.createElement('link')\n chunkHint.rel = 'modulepreload'\n chunkHint.href = href\n document.head.appendChild(chunkHint)\n } catch {\n // modulepreload is a hint, not critical\n }\n}\n\n/**\n * Prefetch a route's JS chunk by injecting `<link rel=\"prefetch\">` into the\n * document head. Deduplicates — calling with the same href twice is a no-op.\n *\n * @example\n * prefetchRoute('/about')\n * prefetchRoute('/dashboard')\n */\nexport function prefetchRoute(href: string): void {\n doPrefetch(href)\n}\n\n/**\n * Composable that provides all link behavior — navigation, prefetching,\n * active state, and viewport observation.\n *\n * Use this for full control when `createLink` is too opinionated.\n *\n * @example\n * function MyLink(props: LinkProps) {\n * const link = useLink(props)\n * return (\n * <button ref={link.ref} class={link.classes()} onClick={link.handleClick}>\n * {props.children}\n * </button>\n * )\n * }\n */\nexport function useLink(props: LinkProps): UseLinkReturn {\n const router = useRouter()\n const elementRef = createRef<HTMLAnchorElement>()\n const strategy = props.prefetch ?? 'hover'\n\n function handleClick(e: MouseEvent) {\n // Call user's onClick first — they may call e.preventDefault()\n if (props.onClick) {\n ;(props.onClick as (e: MouseEvent) => void)(e)\n }\n\n if (\n e.defaultPrevented ||\n e.button !== 0 ||\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n e.altKey ||\n props.external ||\n !props.href\n ) {\n return\n }\n e.preventDefault()\n router.push(props.href)\n }\n\n function handleMouseEnter() {\n if (strategy === 'hover') {\n doPrefetch(props.href)\n }\n }\n\n function handleTouchStart() {\n if (strategy === 'hover' || strategy === 'viewport') {\n doPrefetch(props.href)\n }\n }\n\n if (strategy === 'viewport') {\n useIntersectionObserver(\n () => elementRef.current ?? undefined,\n () => doPrefetch(props.href),\n )\n }\n\n const isActive = () => {\n const currentPath = router.currentRoute()?.path\n if (!currentPath || !props.href) return false\n if (props.href === '/') return currentPath === '/'\n return currentPath.startsWith(props.href)\n }\n\n const isExactActive = () => {\n const currentPath = router.currentRoute()?.path\n if (!currentPath) return false\n return currentPath === props.href\n }\n\n const classes = () => {\n const cls: string[] = []\n if (props.class) cls.push(props.class)\n if (props.activeClass && isActive()) cls.push(props.activeClass)\n if (props.exactActiveClass && isExactActive()) cls.push(props.exactActiveClass)\n return cls.join(' ')\n }\n\n return {\n ref: elementRef,\n handleClick,\n handleMouseEnter,\n handleTouchStart,\n isActive,\n isExactActive,\n classes,\n }\n}\n\n/**\n * Higher-order component that wraps any component with link behavior.\n *\n * The wrapped component receives {@link LinkRenderProps} with all handlers,\n * active state, and accessibility attributes pre-wired.\n *\n * @example\n * // Custom button link\n * const ButtonLink = createLink((props) => (\n * <button\n * ref={props.ref}\n * class={props.class}\n * onClick={props.onClick}\n * onMouseEnter={props.onMouseEnter}\n * >\n * {props.children}\n * </button>\n * ))\n *\n * // Custom styled component\n * const CardLink = createLink((props) => (\n * <div\n * ref={props.ref}\n * class={`card ${props.isActive() ? \"card--active\" : \"\"}`}\n * onClick={props.onClick}\n * onMouseEnter={props.onMouseEnter}\n * >\n * {props.children}\n * </div>\n * ))\n *\n * // Usage\n * <ButtonLink href=\"/about\">About</ButtonLink>\n * <CardLink href=\"/posts\" prefetch=\"viewport\">Posts</CardLink>\n */\nexport function createLink(Component: (props: LinkRenderProps) => any): (props: LinkProps) => any {\n return function WrappedLink(props: LinkProps) {\n const link = useLink(props)\n\n return (\n <Component\n href={props.href}\n ref={link.ref}\n onClick={link.handleClick}\n onMouseEnter={link.handleMouseEnter}\n onTouchStart={link.handleTouchStart}\n isActive={link.isActive}\n isExactActive={link.isExactActive}\n class={link.classes}\n {...(props.style ? { style: props.style } : {})}\n {...(props.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}\n {...(props['aria-label'] ? { 'aria-label': props['aria-label'] } : {})}\n children={props.children}\n />\n )\n }\n}\n\n/**\n * Default navigation link built on an `<a>` tag.\n *\n * @example\n * <Link href=\"/about\" prefetch=\"viewport\">About</Link>\n * <Link href=\"/posts\" activeClass=\"nav-active\">Posts</Link>\n */\nexport const Link = createLink((props: LinkRenderProps) => (\n <a\n ref={props.ref}\n href={props.href}\n {...(props.class ? { class: props.class } : {})}\n {...(props.style ? { style: props.style } : {})}\n {...(props.target ? { target: props.target } : {})}\n {...(props.rel ? { rel: props.rel } : {})}\n {...(props['aria-label'] ? { 'aria-label': props['aria-label'] } : {})}\n {...(props.isExactActive() ? { 'aria-current': 'page' as const } : {})}\n onClick={props.onClick}\n onMouseEnter={props.onMouseEnter}\n onTouchStart={props.onTouchStart}\n >\n {props.children}\n </a>\n))\n","import type { VNodeChild } from '@pyreon/core'\nimport { createRef, onMount, onUnmount } from '@pyreon/core'\nimport { useIntersectionObserver } from './utils/use-intersection-observer'\n\n// ─── Script optimization component ─────────────────────────────────────────\n//\n// <Script> provides optimized third-party script loading:\n// - Defer loading until after hydration\n// - Load on idle (requestIdleCallback)\n// - Load on interaction (click, scroll, etc.)\n// - Load on viewport entry\n// - Worker offloading for analytics scripts\n\nexport interface ScriptProps {\n /** Script source URL. */\n src: string\n /** Loading strategy. Default: \"afterHydration\" */\n strategy?: ScriptStrategy\n /** Inline script content (alternative to src). */\n children?: string\n /** Script id for deduplication. */\n id?: string\n /** Async attribute. Default: true */\n async?: boolean\n /** onLoad callback. */\n onLoad?: () => void\n /** onError callback. */\n onError?: (error: Error) => void\n}\n\nexport type ScriptStrategy =\n | 'beforeHydration'\n | 'afterHydration'\n | 'onIdle'\n | 'onInteraction'\n | 'onViewport'\n\n/**\n * Optimized script loading component.\n *\n * @example\n * // Load analytics after page is interactive\n * <Script src=\"https://analytics.example.com/script.js\" strategy=\"onIdle\" />\n *\n * // Load chat widget when user scrolls\n * <Script src=\"/chat-widget.js\" strategy=\"onViewport\" />\n *\n * // Inline script with deferred execution\n * <Script strategy=\"afterHydration\">\n * {`console.log(\"App hydrated!\")`}\n * </Script>\n */\nexport function Script(props: ScriptProps): VNodeChild {\n function loadScript() {\n // Deduplication\n if (props.id && document.getElementById(props.id)) return\n\n const script = document.createElement('script')\n if (props.src) script.src = props.src\n if (props.id) script.id = props.id\n script.async = props.async !== false\n\n if (props.onLoad) script.onload = props.onLoad\n if (props.onError) {\n script.onerror = () => props.onError?.(new Error(`Failed to load: ${props.src}`))\n }\n\n if (props.children && !props.src) {\n script.textContent = props.children\n }\n\n document.head.appendChild(script)\n }\n\n onMount(() => {\n const strategy = props.strategy ?? 'afterHydration'\n\n switch (strategy) {\n case 'beforeHydration':\n // Already in HTML — do nothing\n break\n\n case 'afterHydration':\n // Load immediately after mount (hydration is complete)\n loadScript()\n break\n\n case 'onIdle':\n if ('requestIdleCallback' in window) {\n requestIdleCallback(() => loadScript(), { timeout: 5000 })\n } else {\n setTimeout(loadScript, 200)\n }\n break\n\n case 'onInteraction': {\n const events = ['click', 'scroll', 'keydown', 'touchstart']\n function handler() {\n for (const e of events) document.removeEventListener(e, handler)\n loadScript()\n }\n for (const e of events) {\n document.addEventListener(e, handler, { once: true, passive: true })\n }\n onUnmount(() => {\n for (const e of events) document.removeEventListener(e, handler)\n })\n break\n }\n\n case 'onViewport':\n // Handled below via useIntersectionObserver on the sentinel element\n break\n }\n return undefined\n })\n\n const sentinelRef = createRef<HTMLElement>()\n const strategy = props.strategy ?? 'afterHydration'\n\n if (strategy === 'onViewport') {\n useIntersectionObserver(\n () => sentinelRef.current ?? undefined,\n () => loadScript(),\n )\n }\n\n if (strategy === 'onViewport') {\n return <div ref={sentinelRef} style=\"width:0;height:0;overflow:hidden\" />\n }\n\n return null\n}\n","import { createContext } from '@pyreon/core'\nimport { signal } from '@pyreon/reactivity'\nimport type { Plugin } from 'vite'\n\n// ─── Localized routing ─────────────────────────────────────────────────────\n//\n// Adds locale-prefixed routes to Zero's file-system router:\n// - /about → /en/about, /de/about, /cs/about\n// - / → /en, /de, /cs (or default locale without prefix)\n// - Automatic locale detection from Accept-Language header\n// - Redirect to preferred locale\n// - hreflang link generation\n//\n// Usage:\n// import { i18nRouting } from \"@pyreon/zero\"\n// export default { plugins: [zero(), i18nRouting({ locales: [\"en\", \"de\"], defaultLocale: \"en\" })] }\n\nexport interface I18nRoutingConfig {\n /** Supported locales. e.g. [\"en\", \"de\", \"cs\"] */\n locales: string[]\n /** Default locale — served without prefix (/ instead of /en/). */\n defaultLocale: string\n /** Redirect root to detected locale. Default: true */\n detectLocale?: boolean\n /** Cookie name to persist locale preference. Default: \"locale\" */\n cookieName?: string\n /** URL strategy. Default: \"prefix-except-default\" */\n strategy?: 'prefix' | 'prefix-except-default'\n}\n\nexport interface LocaleContext {\n /** Current locale code. e.g. \"en\", \"de\" */\n locale: string\n /** All supported locales. */\n locales: string[]\n /** Default locale. */\n defaultLocale: string\n /** Build a localized path. e.g. localePath(\"/about\", \"de\") → \"/de/about\" */\n localePath: (path: string, locale?: string) => string\n /** Get hreflang alternates for the current path. */\n alternates: () => Array<{ locale: string; url: string }>\n}\n\n/**\n * Detect preferred locale from Accept-Language header.\n */\nexport function detectLocaleFromHeader(\n acceptLanguage: string | null | undefined,\n locales: string[],\n defaultLocale: string,\n): string {\n if (!acceptLanguage) return defaultLocale\n\n // Parse Accept-Language: en-US,en;q=0.9,de;q=0.8\n const preferred = acceptLanguage\n .split(',')\n .map((part) => {\n const [lang, q] = part.trim().split(';q=')\n return {\n lang: lang?.split('-')[0]?.toLowerCase() ?? '',\n quality: q ? Number.parseFloat(q) : 1,\n }\n })\n .sort((a, b) => b.quality - a.quality)\n\n for (const { lang } of preferred) {\n if (locales.includes(lang)) return lang\n }\n\n return defaultLocale\n}\n\n/**\n * Extract locale from a URL path.\n * Returns { locale, pathWithoutLocale }.\n */\nexport function extractLocaleFromPath(\n path: string,\n locales: string[],\n defaultLocale: string,\n): { locale: string; pathWithoutLocale: string } {\n const segments = path.split('/').filter(Boolean)\n const firstSegment = segments[0]?.toLowerCase()\n\n if (firstSegment && locales.includes(firstSegment)) {\n return {\n locale: firstSegment,\n pathWithoutLocale: '/' + segments.slice(1).join('/') || '/',\n }\n }\n\n return { locale: defaultLocale, pathWithoutLocale: path }\n}\n\n/**\n * Build a localized path.\n */\nexport function buildLocalePath(\n path: string,\n locale: string,\n defaultLocale: string,\n strategy: 'prefix' | 'prefix-except-default',\n): string {\n const clean = path === '/' ? '' : path\n if (strategy === 'prefix-except-default' && locale === defaultLocale) {\n return path\n }\n return `/${locale}${clean}`\n}\n\n/**\n * Create a LocaleContext for use in components and loaders.\n */\nexport function createLocaleContext(\n locale: string,\n path: string,\n config: I18nRoutingConfig,\n): LocaleContext {\n const strategy = config.strategy ?? 'prefix-except-default'\n\n return {\n locale,\n locales: config.locales,\n defaultLocale: config.defaultLocale,\n\n localePath(targetPath: string, targetLocale?: string) {\n return buildLocalePath(\n targetPath,\n targetLocale ?? locale,\n config.defaultLocale,\n strategy,\n )\n },\n\n alternates() {\n const { pathWithoutLocale } = extractLocaleFromPath(\n path,\n config.locales,\n config.defaultLocale,\n )\n return config.locales.map((loc) => ({\n locale: loc,\n url: buildLocalePath(pathWithoutLocale, loc, config.defaultLocale, strategy),\n }))\n },\n }\n}\n\n/**\n * I18n routing middleware for Zero's server.\n *\n * - Detects locale from URL prefix or Accept-Language header\n * - Redirects root to preferred locale (when detectLocale is true)\n * - Sets locale context for loaders and components\n *\n * @example\n * ```ts\n * // zero.config.ts\n * import { i18nRouting } from \"@pyreon/zero\"\n *\n * export default defineConfig({\n * plugins: [\n * i18nRouting({\n * locales: [\"en\", \"de\", \"cs\"],\n * defaultLocale: \"en\",\n * }),\n * ],\n * })\n * ```\n */\nexport function i18nRouting(config: I18nRoutingConfig): Plugin {\n const strategy = config.strategy ?? 'prefix-except-default'\n const detectEnabled = config.detectLocale !== false\n const cookieName = config.cookieName ?? 'locale'\n\n return {\n name: 'pyreon-zero-i18n-routing',\n\n // Route duplication is NOT handled here. The fs-router's `scanRouteFiles`\n // consumes the i18n config to duplicate routes per locale at build time.\n // This plugin only provides: (1) the server middleware for locale detection\n // and (2) the runtime hooks (useLocale, setLocale) for client-side use.\n configResolved() {},\n\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const url = req.url ?? '/'\n\n // Skip static assets\n if (url.startsWith('/@') || url.startsWith('/__') || url.includes('.')) {\n return next()\n }\n\n const { locale } = extractLocaleFromPath(\n url,\n config.locales,\n config.defaultLocale,\n )\n\n // Redirect root to detected locale\n if (detectEnabled && url === '/') {\n const cookies = parseCookies(req.headers.cookie)\n const preferredFromCookie = cookies[cookieName]\n const preferredFromHeader = detectLocaleFromHeader(\n req.headers['accept-language'],\n config.locales,\n config.defaultLocale,\n )\n const preferred = preferredFromCookie && config.locales.includes(preferredFromCookie)\n ? preferredFromCookie\n : preferredFromHeader\n\n if (strategy === 'prefix' || preferred !== config.defaultLocale) {\n res.writeHead(302, { Location: `/${preferred}/` })\n res.end()\n return\n }\n }\n\n // Attach locale context to request for loaders\n ;(req as any).__locale = locale\n ;(req as any).__localeContext = createLocaleContext(locale, url, config)\n\n // Update the module-level signal so useLocale() returns the correct value\n localeSignal.set(locale)\n\n next()\n })\n },\n }\n}\n\nfunction parseCookies(header: string | undefined): Record<string, string> {\n if (!header) return {}\n const result: Record<string, string> = {}\n for (const pair of header.split(';')) {\n const [key, value] = pair.trim().split('=')\n if (key && value) result[key] = decodeURIComponent(value)\n }\n return result\n}\n\n// ─── Reactive locale hook ───────────────────────────────────────────────────\n\n/** @internal Context for the current locale. */\nexport const LocaleCtx = createContext<string>('en')\n\n/** Current locale signal — set by the server middleware or client-side detection. */\nexport const localeSignal = signal('en')\n\n/**\n * Read the current locale reactively.\n *\n * Returns the locale signal value directly — reactive in both SSR and CSR.\n * The server middleware sets `localeSignal` per-request, and client-side\n * `setLocale()` updates it as well.\n *\n * @example\n * ```tsx\n * const locale = useLocale() // \"en\", \"de\", etc.\n * ```\n */\nexport function useLocale(): string {\n return localeSignal()\n}\n\n/**\n * Set the locale client-side and update the URL.\n *\n * @example\n * ```tsx\n * <button onClick={() => setLocale('de')}>Deutsch</button>\n * ```\n */\nexport function setLocale(\n locale: string,\n config: I18nRoutingConfig,\n): void {\n localeSignal.set(locale)\n\n // Persist to cookie\n if (typeof document !== 'undefined') {\n document.cookie = `${config.cookieName ?? 'locale'}=${locale}; path=/; max-age=31536000`\n }\n\n // Navigate to localized URL — use pushState to avoid full page reload\n if (typeof window !== 'undefined') {\n const strategy = config.strategy ?? 'prefix-except-default'\n const { pathWithoutLocale } = extractLocaleFromPath(\n window.location.pathname,\n config.locales,\n config.defaultLocale,\n )\n const newPath = buildLocalePath(pathWithoutLocale, locale, config.defaultLocale, strategy)\n window.history.pushState(null, '', newPath)\n // Dispatch popstate so @pyreon/router picks up the URL change\n window.dispatchEvent(new PopStateEvent('popstate'))\n }\n}\n","import type { VNodeChild } from '@pyreon/core'\nimport type { UseHeadInput } from '@pyreon/head'\nimport { useHead } from '@pyreon/head'\nimport type { I18nRoutingConfig } from './i18n-routing'\nimport { extractLocaleFromPath } from './i18n-routing'\n\n// ─── Inline helpers (no node:fs dependency) ─────────────────────────────────\n// These are inlined to avoid importing from favicon.ts/og-image.ts which\n// pull in node:fs at the top level — making Meta unsafe for client bundles.\n\n/** Favicon plugin config shape (type-only). */\ninterface FaviconPluginConfig {\n source: string\n themeColor?: string\n manifest?: boolean\n locales?: Record<string, { source: string; darkSource?: string }>\n [key: string]: unknown\n}\n\nfunction faviconLinks(\n locale: string | undefined,\n config: FaviconPluginConfig,\n): Array<{ rel: string; type?: string; sizes?: string; href: string }> {\n const hasLocaleOverride = locale && config.locales?.[locale]\n const prefix = hasLocaleOverride ? `/${locale}` : ''\n const isSvg = (hasLocaleOverride ? config.locales![locale]!.source : config.source).endsWith('.svg')\n const links: Array<{ rel: string; type?: string; sizes?: string; href: string }> = []\n if (isSvg) links.push({ rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon.svg` })\n links.push(\n { rel: 'icon', type: 'image/png', sizes: '32x32', href: `${prefix}/favicon-32x32.png` },\n { rel: 'icon', type: 'image/png', sizes: '16x16', href: `${prefix}/favicon-16x16.png` },\n { rel: 'apple-touch-icon', sizes: '180x180', href: `${prefix}/apple-touch-icon.png` },\n )\n if (config.manifest !== false) links.push({ rel: 'manifest', href: `${prefix}/site.webmanifest` })\n return links\n}\n\nfunction ogImagePath(templateName: string, locale?: string, outDir = 'og', format: 'png' | 'jpeg' = 'png'): string {\n const ext = format === 'jpeg' ? 'jpg' : 'png'\n const suffix = locale ? `-${locale}` : ''\n return `/${outDir}/${templateName}${suffix}.${ext}`\n}\n\n// ─── Meta component ────────────────────────────────────────────────────────\n\nexport interface MetaProps {\n /** Page title. Accepts reactive accessor `() => string`. */\n title?: string | (() => string)\n /** Page description. Accepts reactive accessor. */\n description?: string | (() => string)\n /** Canonical URL. Also sets og:url. */\n canonical?: string\n /** Open Graph image URL. Also sets twitter:image. */\n image?: string\n /** Image alt text for accessibility. */\n imageAlt?: string\n /** Image width in pixels (og:image:width). Helps crawlers layout before loading. */\n imageWidth?: number\n /** Image height in pixels (og:image:height). */\n imageHeight?: number\n /** Open Graph type. Default: \"website\" */\n type?: 'website' | 'article' | 'product' | 'profile'\n /** Site name for og:site_name. */\n siteName?: string\n /** Twitter card type. Default: \"summary_large_image\" */\n twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player'\n /** Twitter @handle. */\n twitterSite?: string\n /** Twitter creator @handle. */\n twitterCreator?: string\n /** Locale. Default: \"en_US\" */\n locale?: string\n /** Alternate locales for hreflang. */\n alternateLocales?: Array<{ locale: string; url: string }>\n /** Robots directives. Default: \"index, follow\" */\n robots?: string\n /** Convenience: set `true` to emit `noindex, nofollow`. Overrides `robots`. */\n noIndex?: boolean\n /** Published time (ISO 8601) for article type. */\n publishedTime?: string\n /** Modified time (ISO 8601) for article type. */\n modifiedTime?: string\n /** Article author. */\n author?: string\n /** Article tags. */\n tags?: string[]\n /** JSON-LD structured data object. */\n jsonLd?: Record<string, unknown>\n /** Additional custom meta tags. */\n extra?: Array<{ name?: string; property?: string; content: string }>\n /**\n * Open Graph video URL. Also sets og:video:type if the URL ends with\n * a known extension (.mp4, .webm).\n */\n video?: string\n /** Video width in pixels. */\n videoWidth?: number\n /** Video height in pixels. */\n videoHeight?: number\n /**\n * Open Graph audio URL.\n */\n audio?: string\n /**\n * I18n routing config — when provided, auto-generates hreflang alternate\n * links for all locales based on the current path.\n * Also sets og:locale and og:locale:alternate.\n */\n i18n?: I18nRoutingConfig\n /** Base URL for building absolute hreflang URLs. e.g. \"https://example.com\" */\n origin?: string\n /**\n * Favicon plugin config — when provided, injects locale-aware favicon\n * `<link>` tags into `<head>`. Uses the current locale to select\n * the correct favicon set.\n */\n favicon?: FaviconPluginConfig\n /**\n * OG image template name — auto-resolves to the correct locale-specific\n * OG image path generated by `ogImagePlugin`.\n * Sets both `og:image` and `twitter:image` unless `image` is also provided.\n */\n ogTemplate?: string\n /** Output directory for OG images. Default: \"og\" */\n ogImageDir?: string\n /** OG image format. Default: \"png\" */\n ogImageFormat?: 'png' | 'jpeg'\n children?: VNodeChild\n}\n\nconst resolveStr = (v: string | (() => string) | undefined): string | undefined =>\n typeof v === 'function' ? v() : v\n\n/**\n * Declarative meta component for SSR-compatible page metadata.\n *\n * Supports reactive title/description — when passed as `() => string` accessors,\n * they are forwarded to `useHead()` as a reactive getter so updates propagate\n * automatically via signal tracking.\n *\n * @example\n * ```tsx\n * <Meta title=\"My Page\" description=\"...\" image=\"/og.jpg\" canonical=\"https://...\" />\n * ```\n *\n * @example Reactive title\n * ```tsx\n * const count = signal(0)\n * <Meta title={() => `${count()} items`} />\n * ```\n */\nexport function Meta(props: MetaProps): VNodeChild {\n const hasReactiveTitle = typeof props.title === 'function'\n const hasReactiveDescription = typeof props.description === 'function'\n\n // If title or description are reactive accessors, pass a getter to useHead\n // so it re-evaluates when the signals change.\n if (hasReactiveTitle || hasReactiveDescription) {\n useHead((): UseHeadInput => {\n const title = resolveStr(props.title)\n const description = resolveStr(props.description)\n const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]\n const tags = buildMetaTags(resolved)\n const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }\n if (title) input.title = title\n return input\n })\n } else {\n const title = resolveStr(props.title)\n const description = resolveStr(props.description)\n const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]\n const tags = buildMetaTags(resolved)\n const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }\n if (title) input.title = title\n useHead(input)\n }\n\n return props.children ?? null\n}\n\ninterface MetaTagEntry {\n name?: string\n property?: string\n content: string\n [key: string]: string | undefined\n}\n\ninterface LinkTagEntry {\n rel: string\n href?: string\n hreflang?: string\n type?: string\n sizes?: string\n [key: string]: string | undefined\n}\n\ninterface ScriptTagEntry {\n type: string\n children: string\n}\n\ninterface MetaTags {\n meta: MetaTagEntry[]\n link: LinkTagEntry[]\n script: ScriptTagEntry[]\n}\n\nexport function buildMetaTags(\n props: Omit<MetaProps, 'title' | 'description' | 'children'> & {\n title?: string\n description?: string\n },\n): MetaTags {\n const meta: MetaTagEntry[] = []\n const link: LinkTagEntry[] = []\n const script: ScriptTagEntry[] = []\n\n const {\n title, description, canonical, imageAlt, imageWidth, imageHeight,\n type = 'website', siteName,\n twitterCard = 'summary_large_image', twitterSite, twitterCreator,\n locale = 'en_US', alternateLocales,\n publishedTime, modifiedTime, author, tags, jsonLd, extra,\n video, videoWidth, videoHeight, audio,\n favicon, ogTemplate, ogImageDir, ogImageFormat,\n } = props\n\n // noIndex convenience overrides robots\n const robots = props.noIndex ? 'noindex, nofollow' : (props.robots ?? 'index, follow')\n\n // Resolve image: explicit `image` prop takes precedence over `ogTemplate`\n const image = props.image ?? (\n ogTemplate\n ? ogImagePath(ogTemplate, locale !== 'en_US' ? locale : undefined, ogImageDir, ogImageFormat)\n : undefined\n )\n\n // Auto-resolve image dimensions for OG template images\n const resolvedImageWidth = imageWidth ?? (ogTemplate && !props.image ? 1200 : undefined)\n const resolvedImageHeight = imageHeight ?? (ogTemplate && !props.image ? 630 : undefined)\n\n if (description) meta.push({ name: 'description', content: description })\n if (robots) meta.push({ name: 'robots', content: robots })\n if (author) meta.push({ name: 'author', content: author })\n\n if (title) meta.push({ property: 'og:title', content: title })\n if (description) meta.push({ property: 'og:description', content: description })\n if (canonical) meta.push({ property: 'og:url', content: canonical })\n if (image) meta.push({ property: 'og:image', content: image })\n if (imageAlt) meta.push({ property: 'og:image:alt', content: imageAlt })\n if (resolvedImageWidth) meta.push({ property: 'og:image:width', content: String(resolvedImageWidth) })\n if (resolvedImageHeight) meta.push({ property: 'og:image:height', content: String(resolvedImageHeight) })\n meta.push({ property: 'og:type', content: type })\n if (siteName) meta.push({ property: 'og:site_name', content: siteName })\n meta.push({ property: 'og:locale', content: locale })\n\n // Video\n if (video) {\n meta.push({ property: 'og:video', content: video })\n if (videoWidth) meta.push({ property: 'og:video:width', content: String(videoWidth) })\n if (videoHeight) meta.push({ property: 'og:video:height', content: String(videoHeight) })\n // Auto-detect video type from extension\n if (video.endsWith('.mp4')) meta.push({ property: 'og:video:type', content: 'video/mp4' })\n else if (video.endsWith('.webm')) meta.push({ property: 'og:video:type', content: 'video/webm' })\n }\n\n // Audio\n if (audio) {\n meta.push({ property: 'og:audio', content: audio })\n }\n\n if (type === 'article') {\n if (publishedTime) meta.push({ property: 'article:published_time', content: publishedTime })\n if (modifiedTime) meta.push({ property: 'article:modified_time', content: modifiedTime })\n if (author) meta.push({ property: 'article:author', content: author })\n if (tags) for (const tag of tags) meta.push({ property: 'article:tag', content: tag })\n }\n\n meta.push({ name: 'twitter:card', content: twitterCard })\n if (title) meta.push({ name: 'twitter:title', content: title })\n if (description) meta.push({ name: 'twitter:description', content: description })\n if (image) meta.push({ name: 'twitter:image', content: image })\n if (imageAlt) meta.push({ name: 'twitter:image:alt', content: imageAlt })\n if (twitterSite) meta.push({ name: 'twitter:site', content: twitterSite })\n if (twitterCreator) meta.push({ name: 'twitter:creator', content: twitterCreator })\n\n if (canonical) link.push({ rel: 'canonical', href: canonical })\n if (alternateLocales) {\n for (const alt of alternateLocales) {\n link.push({ rel: 'alternate', hreflang: alt.locale, href: alt.url })\n }\n }\n\n if (jsonLd) {\n script.push({\n type: 'application/ld+json',\n children: JSON.stringify({ '@context': 'https://schema.org', ...jsonLd }),\n })\n }\n\n if (extra) for (const tag of extra) meta.push(tag)\n\n // I18n: auto-generate hreflang alternates from i18nRouting config\n if (props.i18n) {\n const i18nConfig = props.i18n\n const origin = props.origin ?? ''\n const currentPath = canonical?.replace(origin, '') ?? '/'\n const { pathWithoutLocale } = extractLocaleFromPath(\n currentPath,\n i18nConfig.locales,\n i18nConfig.defaultLocale,\n )\n const strategy = i18nConfig.strategy ?? 'prefix-except-default'\n\n for (const loc of i18nConfig.locales) {\n const localizedPath =\n strategy === 'prefix-except-default' && loc === i18nConfig.defaultLocale\n ? pathWithoutLocale\n : `/${loc}${pathWithoutLocale === '/' ? '' : pathWithoutLocale}`\n\n link.push({\n rel: 'alternate',\n hreflang: loc,\n href: `${origin}${localizedPath}`,\n })\n\n // og:locale:alternate for non-current locales\n if (loc !== locale) {\n meta.push({ property: 'og:locale:alternate', content: loc })\n }\n }\n\n // x-default hreflang pointing to default locale\n link.push({\n rel: 'alternate',\n hreflang: 'x-default',\n href: `${origin}${pathWithoutLocale}`,\n })\n }\n\n // Favicon: inject locale-aware favicon links\n if (favicon) {\n const faviconLocale = locale !== 'en_US' ? locale : undefined\n for (const fl of faviconLinks(faviconLocale, favicon)) {\n link.push(fl as LinkTagEntry)\n }\n // Theme color meta from favicon config\n if (favicon.themeColor) {\n meta.push({ name: 'theme-color', content: favicon.themeColor })\n }\n }\n\n return { meta, link, script }\n}\n","import type { VNodeChild } from '@pyreon/core'\nimport { onMount, onUnmount } from '@pyreon/core'\nimport { effect, signal } from '@pyreon/reactivity'\n\n// ─── Theme system ───────────────────────────────────────────────────────────\n//\n// Provides dark/light/system theme support with:\n// - System preference detection via matchMedia\n// - Persistent preference via localStorage\n// - No flash of wrong theme (inline script in HTML)\n// - Reactive theme signal for components\n\nexport type Theme = 'light' | 'dark' | 'system'\n\nconst STORAGE_KEY = 'zero-theme'\n\n/** Reactive theme signal. */\nexport const theme = signal<Theme>('system')\n\n/** SSR fallback when system preference can't be detected. Default: 'light'. */\nlet _ssrDefault: 'light' | 'dark' = 'light'\n\n/**\n * Set the default theme for SSR (when `matchMedia` is unavailable).\n * Call once at server startup before rendering.\n */\nexport function setSSRThemeDefault(value: 'light' | 'dark'): void {\n _ssrDefault = value\n}\n\n/** Computed resolved theme (what's actually applied). */\nexport function resolvedTheme(): 'light' | 'dark' {\n const t = theme()\n if (t === 'system') {\n if (typeof window === 'undefined') return _ssrDefault\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n }\n return t\n}\n\n/** Toggle between light and dark. */\nexport function toggleTheme() {\n const current = resolvedTheme()\n setTheme(current === 'dark' ? 'light' : 'dark')\n}\n\n/** Set theme explicitly. */\nexport function setTheme(t: Theme) {\n theme.set(t)\n if (typeof document !== 'undefined') {\n document.documentElement.dataset.theme = resolvedTheme()\n try {\n localStorage.setItem(STORAGE_KEY, t)\n } catch {\n // localStorage may not be available (SSR, private browsing)\n }\n }\n}\n\n/**\n * Initialize the theme system. Call once in your app entry or layout.\n * Reads from localStorage, listens for system preference changes.\n */\nexport function initTheme() {\n onMount(() => {\n // Read persisted preference\n try {\n const stored = localStorage.getItem(STORAGE_KEY) as Theme | null\n if (stored === 'light' || stored === 'dark' || stored === 'system') {\n theme.set(stored)\n }\n } catch {\n // localStorage may not be available\n }\n\n // Apply to document\n document.documentElement.dataset.theme = resolvedTheme()\n\n // Watch for system preference changes\n const mq = window.matchMedia('(prefers-color-scheme: dark)')\n function onChange() {\n if (theme() === 'system') {\n document.documentElement.dataset.theme = resolvedTheme()\n }\n }\n mq.addEventListener('change', onChange)\n onUnmount(() => mq.removeEventListener('change', onChange))\n\n // Re-apply when theme signal changes — updates data-theme + favicons\n const dispose = effect(() => {\n const mode = resolvedTheme()\n document.documentElement.dataset.theme = mode\n\n // Swap favicon variants (if dual-variant favicons are present)\n const faviconLinks = document.querySelectorAll<HTMLLinkElement>('[data-favicon-theme]')\n for (const link of faviconLinks) {\n link.media = link.dataset.faviconTheme === mode ? '' : 'not all'\n }\n })\n if (dispose) onUnmount(() => dispose.dispose())\n\n return undefined\n })\n}\n\n/**\n * Theme toggle button component.\n *\n * @example\n * import { ThemeToggle } from \"@pyreon/zero/theme\"\n * <ThemeToggle />\n */\nexport function ThemeToggle(props: { class?: string; style?: string }): VNodeChild {\n initTheme()\n\n return (\n <button\n class={props.class}\n style={props.style}\n onClick={toggleTheme}\n aria-label=\"Toggle theme\"\n title=\"Toggle theme\"\n type=\"button\"\n >\n {() =>\n resolvedTheme() === 'dark' ? (\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"5\" />\n <line x1=\"12\" y1=\"1\" x2=\"12\" y2=\"3\" />\n <line x1=\"12\" y1=\"21\" x2=\"12\" y2=\"23\" />\n <line x1=\"4.22\" y1=\"4.22\" x2=\"5.64\" y2=\"5.64\" />\n <line x1=\"18.36\" y1=\"18.36\" x2=\"19.78\" y2=\"19.78\" />\n <line x1=\"1\" y1=\"12\" x2=\"3\" y2=\"12\" />\n <line x1=\"21\" y1=\"12\" x2=\"23\" y2=\"12\" />\n <line x1=\"4.22\" y1=\"19.78\" x2=\"5.64\" y2=\"18.36\" />\n <line x1=\"18.36\" y1=\"5.64\" x2=\"19.78\" y2=\"4.22\" />\n </svg>\n ) : (\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\" />\n </svg>\n )\n }\n </button>\n )\n}\n\n/**\n * Inline script to prevent flash of wrong theme.\n * Include this in your index.html <head> BEFORE any stylesheets.\n *\n * @example\n * // index.html\n * <head>\n * <script>{themeScript}</script>\n * ...\n * </head>\n */\nexport const themeScript = `(function(){try{var t=localStorage.getItem(\"${STORAGE_KEY}\");var r=t===\"light\"?\"light\":t===\"dark\"?\"dark\":window.matchMedia(\"(prefers-color-scheme:dark)\").matches?\"dark\":\"light\";document.documentElement.dataset.theme=r;document.querySelectorAll(\"[data-favicon-theme]\").forEach(function(l){l.media=l.dataset.faviconTheme===r?\"\":\"not all\"})}catch(e){}})()`\n","/**\n * @pyreon/zero — client-safe exports.\n *\n * This entry contains only browser-safe components and hooks.\n * No node:fs, node:path, or other server-only imports.\n *\n * For server/build-time features, use subpath imports:\n * import { faviconPlugin } from \"@pyreon/zero/favicon\"\n * import { createServer } from \"@pyreon/zero/server\"\n * import { defineConfig } from \"@pyreon/zero/config\"\n * import { validateEnv } from \"@pyreon/zero/env\"\n */\n\n// ─── Components (browser-safe) ──────────────────────────────────────────────\n\nexport type { ImageProps, ImageSource } from \"./image\";\nexport { Image } from \"./image\";\nexport type { LinkProps, LinkRenderProps, UseLinkReturn } from \"./link\";\nexport { createLink, Link, prefetchRoute, useLink } from \"./link\";\nexport type { ScriptProps, ScriptStrategy } from \"./script\";\nexport { Script } from \"./script\";\nexport type { MetaProps } from \"./meta\";\nexport { buildMetaTags, Meta } from \"./meta\";\n\n// ─── Theme (browser-safe) ───────────────────────────────────────────────────\n\nexport type { Theme } from \"./theme\";\nexport {\n\tinitTheme,\n\tresolvedTheme,\n\tsetSSRThemeDefault,\n\tsetTheme,\n\tThemeToggle,\n\ttheme,\n\tthemeScript,\n\ttoggleTheme,\n} from \"./theme\";\n\n// ─── I18n hooks (browser-safe) ──────────────────────────────────────────────\n\nexport type { I18nRoutingConfig, LocaleContext } from \"./i18n-routing\";\nexport {\n\tbuildLocalePath,\n\textractLocaleFromPath,\n\tsetLocale,\n\tuseLocale,\n} from \"./i18n-routing\";\n\n// ─── Server-only stubs ──────────────────────────────────────────────────────\n// Throw clear error messages when developers accidentally import server-only\n// APIs from the main entry. These are tree-shaken if not imported.\n\nfunction serverOnly(name: string, subpath: string): never {\n throw new Error(\n `[Pyreon] \"${name}\" is server-only and cannot be imported from \"@pyreon/zero\".\\n` +\n `Import from the subpath instead:\\n\\n` +\n ` import { ${name} } from \"@pyreon/zero/${subpath}\"\\n`,\n )\n}\n\n/* eslint-disable @typescript-eslint/no-unused-vars */\n/** @deprecated Import from `@pyreon/zero/favicon` instead */\nexport function faviconPlugin(..._: unknown[]): never { return serverOnly('faviconPlugin', 'favicon') }\n/** @deprecated Import from `@pyreon/zero/seo` instead */\nexport function seoPlugin(..._: unknown[]): never { return serverOnly('seoPlugin', 'seo') }\n/** @deprecated Import from `@pyreon/zero/server` instead */\nexport function createServer(..._: unknown[]): never { return serverOnly('createServer', 'server') }\n/** @deprecated Import from `@pyreon/zero/config` instead */\nexport function defineConfig(..._: unknown[]): never { return serverOnly('defineConfig', 'config') }\n/** @deprecated Import from `@pyreon/zero/env` instead */\nexport function validateEnv(..._: unknown[]): never { return serverOnly('validateEnv', 'env') }\n/** @deprecated Import from `@pyreon/zero/og-image` instead */\nexport function ogImagePlugin(..._: unknown[]): never { return serverOnly('ogImagePlugin', 'og-image') }\n/** @deprecated Import from `@pyreon/zero/ai` instead */\nexport function aiPlugin(..._: unknown[]): never { return serverOnly('aiPlugin', 'ai') }\n\n// ─── Types (no runtime, safe everywhere) ────────────────────────────────────\n\nexport type {\n\tAdapter,\n\tAdapterBuildOptions,\n\tFileRoute,\n\tISRConfig,\n\tLoaderContext,\n\tRenderMode,\n\tRouteMeta,\n\tRouteMiddlewareEntry,\n\tRouteModule,\n\tZeroConfig,\n} from \"./types\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../../../core/core/lib/jsx-runtime.js","../src/image.tsx","../src/link.tsx","../src/script.tsx","../src/i18n-routing.ts","../src/meta.tsx","../src/theme.tsx","../src/index.ts"],"sourcesContent":["import { onMount, onUnmount } from '@pyreon/core'\n\n/**\n * Observes an element and calls `onIntersect` once it enters the viewport.\n * Automatically disconnects after the first intersection.\n *\n * @param getElement - Getter for the target element (may be undefined before mount).\n * @param onIntersect - Callback fired when the element becomes visible.\n * @param rootMargin - IntersectionObserver rootMargin. Default: \"200px\".\n */\nexport function useIntersectionObserver(\n getElement: () => HTMLElement | undefined,\n onIntersect: () => void,\n rootMargin = '200px',\n) {\n onMount(() => {\n const el = getElement()\n if (!el) return undefined\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n onIntersect()\n observer.disconnect()\n }\n }\n },\n { rootMargin },\n )\n\n observer.observe(el)\n onUnmount(() => observer.disconnect())\n return undefined\n })\n}\n","//#region src/h.ts\n/** Marker for fragment nodes — renders children without a wrapper element */\nconst Fragment = Symbol(\"Pyreon.Fragment\");\n/**\n* Hyperscript function — the compiled output of JSX.\n* `<div class=\"x\">hello</div>` → `h(\"div\", { class: \"x\" }, \"hello\")`\n*\n* Generic on P so TypeScript validates props match the component's signature\n* at the call site, then stores the result in the loosely-typed VNode.\n*/\n/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */\nconst EMPTY_PROPS = {};\nfunction h(type, props, ...children) {\n\treturn {\n\t\ttype,\n\t\tprops: props ?? EMPTY_PROPS,\n\t\tchildren: normalizeChildren(children),\n\t\tkey: props?.key ?? null\n\t};\n}\nfunction normalizeChildren(children) {\n\tfor (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);\n\treturn children;\n}\nfunction flattenChildren(children) {\n\tconst result = [];\n\tfor (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));\n\telse result.push(child);\n\treturn result;\n}\n\n//#endregion\n//#region src/jsx-runtime.ts\n/**\n* JSX automatic runtime.\n*\n* When tsconfig has `\"jsxImportSource\": \"@pyreon/core\"`, the TS/bundler compiler\n* rewrites JSX to imports from this file automatically:\n* <div class=\"x\" /> → jsx(\"div\", { class: \"x\" })\n*/\nfunction jsx(type, props, key) {\n\tconst { children, ...rest } = props;\n\tconst propsWithKey = key != null ? {\n\t\t...rest,\n\t\tkey\n\t} : rest;\n\tif (typeof type === \"function\") return h(type, children !== void 0 ? {\n\t\t...propsWithKey,\n\t\tchildren\n\t} : propsWithKey);\n\treturn h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);\n}\nconst jsxs = jsx;\n\n//#endregion\nexport { Fragment, jsx, jsxs };\n//# sourceMappingURL=jsx-runtime.js.map","import type { VNodeChild } from '@pyreon/core'\nimport { createRef } from '@pyreon/core'\nimport { signal } from '@pyreon/reactivity'\nimport type { FormatSource } from './image-plugin'\nimport { useIntersectionObserver } from './utils/use-intersection-observer'\n\n// ─── Image optimization component ───────────────────────────────────────────\n//\n// <Image> provides:\n// - Lazy loading via IntersectionObserver (loads when near viewport)\n// - Automatic width/height to prevent CLS (Cumulative Layout Shift)\n// - Responsive srcset generation from width descriptors\n// - Multi-format support via <picture> (WebP/AVIF with fallback)\n// - Blur-up placeholder while loading\n// - Priority loading for above-the-fold images\n\nexport interface ImageProps {\n /** Image source URL. */\n src: string\n /** Alt text (required for accessibility). */\n alt: string\n /** Intrinsic width of the image. */\n width: number\n /** Intrinsic height of the image. */\n height: number\n /** Responsive sizes attribute. Default: \"100vw\" */\n sizes?: string\n /** Responsive srcset string or source array. */\n srcset?: string | ImageSource[]\n /** Per-format source sets for <picture>. Provided automatically by imagePlugin. */\n formats?: FormatSource[]\n /** Loading strategy. \"lazy\" uses IntersectionObserver, \"eager\" loads immediately. Default: \"lazy\" */\n loading?: 'lazy' | 'eager'\n /** Mark as priority (LCP image). Disables lazy loading, adds fetchPriority=\"high\". */\n priority?: boolean\n /** Low-quality placeholder image URL or base64 data URI for blur-up effect. */\n placeholder?: string\n /** CSS class name. */\n class?: string\n /** Inline styles. */\n style?: string\n /** CSS object-fit. Default: \"cover\" */\n fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'\n /** Decode async. Default: true */\n decoding?: 'sync' | 'async' | 'auto'\n /**\n * Raw mode — renders a plain `<img>` without the container div,\n * aspect-ratio, max-width, or lazy loading wrapper.\n * Use when the Image is inside a custom layout (absolute positioning, etc.).\n */\n raw?: boolean\n}\n\nexport interface ImageSource {\n src: string\n width: number\n}\n\n/**\n * Optimized image component with lazy loading, responsive images,\n * multi-format <picture> support, and blur-up placeholders.\n *\n * @example\n * // With imagePlugin — spread the import directly\n * import hero from \"./hero.jpg?optimize\"\n * <Image {...hero} alt=\"Hero\" priority />\n *\n * @example\n * // Manual usage\n * <Image src=\"/hero.jpg\" alt=\"Hero\" width={1200} height={630} />\n */\nexport function Image(props: ImageProps): VNodeChild {\n // Raw mode: plain <img> without container, lazy loading, or layout constraints\n if (props.raw) {\n return (\n <img\n src={props.src}\n alt={props.alt}\n width={props.width}\n height={props.height}\n class={props.class}\n style={props.style}\n decoding={props.decoding ?? 'async'}\n loading={props.loading ?? 'lazy'}\n fetchPriority={props.priority ? 'high' : undefined}\n />\n ) as any\n }\n\n const isEager = props.priority || props.loading === 'eager'\n const loaded = signal(isEager)\n const inView = signal(isEager)\n const containerRef = createRef<HTMLElement>()\n\n // Resolve srcset from string or array\n const resolvedSrcset =\n typeof props.srcset === 'string'\n ? props.srcset\n : props.srcset?.map((s) => `${s.src} ${s.width}w`).join(', ')\n\n const sizes = props.sizes ?? '100vw'\n const fit = props.fit ?? 'cover'\n const hasFormats = props.formats && props.formats.length > 0\n const aspectRatio = `${props.width} / ${props.height}`\n\n if (!isEager) {\n useIntersectionObserver(\n () => containerRef.current ?? undefined,\n () => inView.set(true),\n )\n }\n\n // Static styles (don't depend on signals)\n const containerStyle = [\n 'position: relative',\n 'overflow: hidden',\n `aspect-ratio: ${aspectRatio}`,\n `max-width: ${props.width}px`,\n 'width: 100%',\n props.style,\n ]\n .filter(Boolean)\n .join('; ')\n\n const imgEl = (\n <img\n src={() => (inView() ? props.src : '')}\n srcSet={() => (!hasFormats && inView() && resolvedSrcset ? resolvedSrcset : '')}\n sizes={resolvedSrcset ? sizes : undefined}\n alt={props.alt}\n width={props.width}\n height={props.height}\n loading={isEager ? 'eager' : 'lazy'}\n decoding={props.decoding ?? 'async'}\n fetchPriority={props.priority ? 'high' : undefined}\n onLoad={() => loaded.set(true)}\n style={() =>\n [\n 'display: block',\n 'width: 100%',\n 'height: 100%',\n `object-fit: ${fit}`,\n 'transition: opacity 0.3s ease',\n props.placeholder && !loaded() ? 'opacity: 0' : 'opacity: 1',\n ].join('; ')\n }\n />\n )\n\n return (\n <div ref={containerRef} class={props.class} style={containerStyle}>\n {props.placeholder && (\n <img\n src={props.placeholder}\n alt=\"\"\n aria-hidden=\"true\"\n loading=\"eager\"\n style={() =>\n [\n 'position: absolute',\n 'inset: 0',\n 'width: 100%',\n 'height: 100%',\n 'object-fit: cover',\n 'filter: blur(20px)',\n 'transform: scale(1.1)',\n 'transition: opacity 0.4s ease',\n loaded() ? 'opacity: 0; pointer-events: none' : 'opacity: 1',\n ].join('; ')\n }\n />\n )}\n {hasFormats ? (\n <picture>\n {props.formats?.map((fmt) => (\n <source\n type={fmt.type}\n srcSet={() => (inView() ? (fmt.srcset ?? '') : '')}\n sizes={sizes}\n />\n ))}\n {imgEl}\n </picture>\n ) : (\n imgEl\n )}\n </div>\n )\n}\n","import { createRef } from '@pyreon/core'\nimport { useRouter } from '@pyreon/router'\nimport { useIntersectionObserver } from './utils/use-intersection-observer'\n\n// ─── Link component with prefetching ────────────────────────────────────────\n//\n// Provides client-side navigation, prefetching, and active state tracking.\n// Three levels of API:\n//\n// 1. useLink(props) — composable returning handlers, state, and ref callback\n// 2. createLink(Comp) — HOC wrapping any component with link behavior\n// 3. Link — default <a>-based link (built on createLink)\n\nexport interface LinkProps {\n /** Target URL path. */\n href: string\n /** Link content. */\n children?: any\n /** CSS class name. */\n class?: string\n /** Class applied when this link matches the current route. */\n activeClass?: string\n /** Class applied when this link exactly matches the current route. */\n exactActiveClass?: string\n /** Prefetch strategy. Default: \"hover\" */\n prefetch?: 'hover' | 'viewport' | 'none'\n /** Open in new tab. */\n external?: boolean\n /** Inline styles. */\n style?: string\n /** ARIA label. */\n 'aria-label'?: string\n /** Additional click handler — called before navigation. Call e.preventDefault() to cancel. */\n onClick?: ((e: MouseEvent) => void) | undefined\n}\n\n/** Props passed to a custom component via createLink. */\nexport interface LinkRenderProps {\n href: string\n ref: import('@pyreon/core').Ref<HTMLAnchorElement>\n onClick: (e: MouseEvent) => void\n onMouseEnter: () => void\n onTouchStart: () => void\n isActive: () => boolean\n isExactActive: () => boolean\n /** Reactive class string — pass directly to element for auto-updates on route change. */\n class: (() => string) | string | undefined\n style?: string\n target?: string\n rel?: string\n 'aria-label'?: string\n children?: any\n}\n\n/** Return type of useLink. */\nexport interface UseLinkReturn {\n /** Ref object — attach to the root element for viewport-based prefetch. */\n ref: import('@pyreon/core').Ref<HTMLAnchorElement>\n /** Click handler — performs client-side navigation. */\n handleClick: (e: MouseEvent) => void\n /** Mouse enter handler — triggers hover prefetch. */\n handleMouseEnter: () => void\n /** Touch start handler — triggers prefetch on mobile. */\n handleTouchStart: () => void\n /** Whether the link partially matches the current route. */\n isActive: () => boolean\n /** Whether the link exactly matches the current route. */\n isExactActive: () => boolean\n /** Resolved class string including active classes. */\n classes: () => string\n}\n\nconst MAX_PREFETCH_CACHE = 200\nconst prefetched = new Set<string>()\n\nfunction doPrefetch(href: string) {\n // Prefetch only fires from browser-mounted Link interactions (hover /\n // click intent). Explicit guard documents the SSR-safety contract.\n if (typeof document === 'undefined') return\n if (prefetched.has(href)) return\n // Evict oldest entries when cache is full\n if (prefetched.size >= MAX_PREFETCH_CACHE) {\n const first = prefetched.values().next().value\n if (first) prefetched.delete(first)\n }\n prefetched.add(href)\n\n const docLink = document.createElement('link')\n docLink.rel = 'prefetch'\n docLink.href = href\n docLink.as = 'document'\n document.head.appendChild(docLink)\n\n try {\n const chunkHint = document.createElement('link')\n chunkHint.rel = 'modulepreload'\n chunkHint.href = href\n document.head.appendChild(chunkHint)\n } catch {\n // modulepreload is a hint, not critical\n }\n}\n\n/**\n * Prefetch a route's JS chunk by injecting `<link rel=\"prefetch\">` into the\n * document head. Deduplicates — calling with the same href twice is a no-op.\n *\n * @example\n * prefetchRoute('/about')\n * prefetchRoute('/dashboard')\n */\nexport function prefetchRoute(href: string): void {\n doPrefetch(href)\n}\n\n/**\n * Composable that provides all link behavior — navigation, prefetching,\n * active state, and viewport observation.\n *\n * Use this for full control when `createLink` is too opinionated.\n *\n * @example\n * function MyLink(props: LinkProps) {\n * const link = useLink(props)\n * return (\n * <button ref={link.ref} class={link.classes()} onClick={link.handleClick}>\n * {props.children}\n * </button>\n * )\n * }\n */\nexport function useLink(props: LinkProps): UseLinkReturn {\n const router = useRouter()\n const elementRef = createRef<HTMLAnchorElement>()\n const strategy = props.prefetch ?? 'hover'\n\n function handleClick(e: MouseEvent) {\n // Call user's onClick first — they may call e.preventDefault()\n if (props.onClick) {\n ;(props.onClick as (e: MouseEvent) => void)(e)\n }\n\n if (\n e.defaultPrevented ||\n e.button !== 0 ||\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n e.altKey ||\n props.external ||\n !props.href\n ) {\n return\n }\n e.preventDefault()\n router.push(props.href)\n }\n\n function handleMouseEnter() {\n if (strategy === 'hover') {\n doPrefetch(props.href)\n }\n }\n\n function handleTouchStart() {\n if (strategy === 'hover' || strategy === 'viewport') {\n doPrefetch(props.href)\n }\n }\n\n if (strategy === 'viewport') {\n useIntersectionObserver(\n () => elementRef.current ?? undefined,\n () => doPrefetch(props.href),\n )\n }\n\n const isActive = () => {\n const currentPath = router.currentRoute()?.path\n if (!currentPath || !props.href) return false\n if (props.href === '/') return currentPath === '/'\n return currentPath.startsWith(props.href)\n }\n\n const isExactActive = () => {\n const currentPath = router.currentRoute()?.path\n if (!currentPath) return false\n return currentPath === props.href\n }\n\n const classes = () => {\n const cls: string[] = []\n if (props.class) cls.push(props.class)\n if (props.activeClass && isActive()) cls.push(props.activeClass)\n if (props.exactActiveClass && isExactActive()) cls.push(props.exactActiveClass)\n return cls.join(' ')\n }\n\n return {\n ref: elementRef,\n handleClick,\n handleMouseEnter,\n handleTouchStart,\n isActive,\n isExactActive,\n classes,\n }\n}\n\n/**\n * Higher-order component that wraps any component with link behavior.\n *\n * The wrapped component receives {@link LinkRenderProps} with all handlers,\n * active state, and accessibility attributes pre-wired.\n *\n * @example\n * // Custom button link\n * const ButtonLink = createLink((props) => (\n * <button\n * ref={props.ref}\n * class={props.class}\n * onClick={props.onClick}\n * onMouseEnter={props.onMouseEnter}\n * >\n * {props.children}\n * </button>\n * ))\n *\n * // Custom styled component\n * const CardLink = createLink((props) => (\n * <div\n * ref={props.ref}\n * class={`card ${props.isActive() ? \"card--active\" : \"\"}`}\n * onClick={props.onClick}\n * onMouseEnter={props.onMouseEnter}\n * >\n * {props.children}\n * </div>\n * ))\n *\n * // Usage\n * <ButtonLink href=\"/about\">About</ButtonLink>\n * <CardLink href=\"/posts\" prefetch=\"viewport\">Posts</CardLink>\n */\nexport function createLink(Component: (props: LinkRenderProps) => any): (props: LinkProps) => any {\n return function WrappedLink(props: LinkProps) {\n const link = useLink(props)\n\n return (\n <Component\n href={props.href}\n ref={link.ref}\n onClick={link.handleClick}\n onMouseEnter={link.handleMouseEnter}\n onTouchStart={link.handleTouchStart}\n isActive={link.isActive}\n isExactActive={link.isExactActive}\n class={link.classes}\n {...(props.style ? { style: props.style } : {})}\n {...(props.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}\n {...(props['aria-label'] ? { 'aria-label': props['aria-label'] } : {})}\n children={props.children}\n />\n )\n }\n}\n\n/**\n * Default navigation link built on an `<a>` tag.\n *\n * @example\n * <Link href=\"/about\" prefetch=\"viewport\">About</Link>\n * <Link href=\"/posts\" activeClass=\"nav-active\">Posts</Link>\n */\nexport const Link = createLink((props: LinkRenderProps) => (\n <a\n ref={props.ref}\n href={props.href}\n {...(props.class ? { class: props.class } : {})}\n {...(props.style ? { style: props.style } : {})}\n {...(props.target ? { target: props.target } : {})}\n {...(props.rel ? { rel: props.rel } : {})}\n {...(props['aria-label'] ? { 'aria-label': props['aria-label'] } : {})}\n {...(props.isExactActive() ? { 'aria-current': 'page' as const } : {})}\n onClick={props.onClick}\n onMouseEnter={props.onMouseEnter}\n onTouchStart={props.onTouchStart}\n >\n {props.children}\n </a>\n))\n","import type { VNodeChild } from '@pyreon/core'\nimport { createRef, onMount, onUnmount } from '@pyreon/core'\nimport { useIntersectionObserver } from './utils/use-intersection-observer'\n\n// ─── Script optimization component ─────────────────────────────────────────\n//\n// <Script> provides optimized third-party script loading:\n// - Defer loading until after hydration\n// - Load on idle (requestIdleCallback)\n// - Load on interaction (click, scroll, etc.)\n// - Load on viewport entry\n// - Worker offloading for analytics scripts\n\nexport interface ScriptProps {\n /** Script source URL. */\n src: string\n /** Loading strategy. Default: \"afterHydration\" */\n strategy?: ScriptStrategy\n /** Inline script content (alternative to src). */\n children?: string\n /** Script id for deduplication. */\n id?: string\n /** Async attribute. Default: true */\n async?: boolean\n /** onLoad callback. */\n onLoad?: () => void\n /** onError callback. */\n onError?: (error: Error) => void\n}\n\nexport type ScriptStrategy =\n | 'beforeHydration'\n | 'afterHydration'\n | 'onIdle'\n | 'onInteraction'\n | 'onViewport'\n\n/**\n * Optimized script loading component.\n *\n * @example\n * // Load analytics after page is interactive\n * <Script src=\"https://analytics.example.com/script.js\" strategy=\"onIdle\" />\n *\n * // Load chat widget when user scrolls\n * <Script src=\"/chat-widget.js\" strategy=\"onViewport\" />\n *\n * // Inline script with deferred execution\n * <Script strategy=\"afterHydration\">\n * {`console.log(\"App hydrated!\")`}\n * </Script>\n */\nexport function Script(props: ScriptProps): VNodeChild {\n function loadScript() {\n // Only invoked from `onMount` — explicit guard documents the\n // SSR-safety contract at the callsite (the rule can't AST-trace the\n // indirect call).\n if (typeof document === 'undefined') return\n // Deduplication\n if (props.id && document.getElementById(props.id)) return\n\n const script = document.createElement('script')\n if (props.src) script.src = props.src\n if (props.id) script.id = props.id\n script.async = props.async !== false\n\n if (props.onLoad) script.onload = props.onLoad\n if (props.onError) {\n script.onerror = () => props.onError?.(new Error(`Failed to load: ${props.src}`))\n }\n\n if (props.children && !props.src) {\n script.textContent = props.children\n }\n\n document.head.appendChild(script)\n }\n\n onMount(() => {\n const strategy = props.strategy ?? 'afterHydration'\n\n switch (strategy) {\n case 'beforeHydration':\n // Already in HTML — do nothing\n break\n\n case 'afterHydration':\n // Load immediately after mount (hydration is complete)\n loadScript()\n break\n\n case 'onIdle':\n if ('requestIdleCallback' in window) {\n requestIdleCallback(() => loadScript(), { timeout: 5000 })\n } else {\n setTimeout(loadScript, 200)\n }\n break\n\n case 'onInteraction': {\n const events = ['click', 'scroll', 'keydown', 'touchstart']\n function handler() {\n for (const e of events) document.removeEventListener(e, handler)\n loadScript()\n }\n for (const e of events) {\n document.addEventListener(e, handler, { once: true, passive: true })\n }\n onUnmount(() => {\n for (const e of events) document.removeEventListener(e, handler)\n })\n break\n }\n\n case 'onViewport':\n // Handled below via useIntersectionObserver on the sentinel element\n break\n }\n return undefined\n })\n\n const sentinelRef = createRef<HTMLElement>()\n const strategy = props.strategy ?? 'afterHydration'\n\n if (strategy === 'onViewport') {\n useIntersectionObserver(\n () => sentinelRef.current ?? undefined,\n () => loadScript(),\n )\n }\n\n if (strategy === 'onViewport') {\n return <div ref={sentinelRef} style=\"width:0;height:0;overflow:hidden\" />\n }\n\n return null\n}\n","import { createContext } from '@pyreon/core'\nimport { signal } from '@pyreon/reactivity'\nimport type { Plugin } from 'vite'\n\n// ─── Localized routing ─────────────────────────────────────────────────────\n//\n// Adds locale-prefixed routes to Zero's file-system router:\n// - /about → /en/about, /de/about, /cs/about\n// - / → /en, /de, /cs (or default locale without prefix)\n// - Automatic locale detection from Accept-Language header\n// - Redirect to preferred locale\n// - hreflang link generation\n//\n// Usage:\n// import { i18nRouting } from \"@pyreon/zero\"\n// export default { plugins: [Pyreon], defaultLocale: \"en\" })] }\n\nexport interface I18nRoutingConfig {\n /** Supported locales. e.g. [\"en\", \"de\", \"cs\"] */\n locales: string[]\n /** Default locale — served without prefix (/ instead of /en/). */\n defaultLocale: string\n /** Redirect root to detected locale. Default: true */\n detectLocale?: boolean\n /** Cookie name to persist locale preference. Default: \"locale\" */\n cookieName?: string\n /** URL strategy. Default: \"prefix-except-default\" */\n strategy?: 'prefix' | 'prefix-except-default'\n}\n\nexport interface LocaleContext {\n /** Current locale code. e.g. \"en\", \"de\" */\n locale: string\n /** All supported locales. */\n locales: string[]\n /** Default locale. */\n defaultLocale: string\n /** Build a localized path. e.g. localePath(\"/about\", \"de\") → \"/de/about\" */\n localePath: (path: string, locale?: string) => string\n /** Get hreflang alternates for the current path. */\n alternates: () => Array<{ locale: string; url: string }>\n}\n\n/**\n * Detect preferred locale from Accept-Language header.\n */\nexport function detectLocaleFromHeader(\n acceptLanguage: string | null | undefined,\n locales: string[],\n defaultLocale: string,\n): string {\n if (!acceptLanguage) return defaultLocale\n\n // Parse Accept-Language: en-US,en;q=0.9,de;q=0.8\n const preferred = acceptLanguage\n .split(',')\n .map((part) => {\n const [lang, q] = part.trim().split(';q=')\n return {\n lang: lang?.split('-')[0]?.toLowerCase() ?? '',\n quality: q ? Number.parseFloat(q) : 1,\n }\n })\n .sort((a, b) => b.quality - a.quality)\n\n for (const { lang } of preferred) {\n if (locales.includes(lang)) return lang\n }\n\n return defaultLocale\n}\n\n/**\n * Extract locale from a URL path.\n * Returns { locale, pathWithoutLocale }.\n */\nexport function extractLocaleFromPath(\n path: string,\n locales: string[],\n defaultLocale: string,\n): { locale: string; pathWithoutLocale: string } {\n const segments = path.split('/').filter(Boolean)\n const firstSegment = segments[0]?.toLowerCase()\n\n if (firstSegment && locales.includes(firstSegment)) {\n return {\n locale: firstSegment,\n pathWithoutLocale: '/' + segments.slice(1).join('/') || '/',\n }\n }\n\n return { locale: defaultLocale, pathWithoutLocale: path }\n}\n\n/**\n * Build a localized path.\n */\nexport function buildLocalePath(\n path: string,\n locale: string,\n defaultLocale: string,\n strategy: 'prefix' | 'prefix-except-default',\n): string {\n const clean = path === '/' ? '' : path\n if (strategy === 'prefix-except-default' && locale === defaultLocale) {\n return path\n }\n return `/${locale}${clean}`\n}\n\n/**\n * Create a LocaleContext for use in components and loaders.\n */\nexport function createLocaleContext(\n locale: string,\n path: string,\n config: I18nRoutingConfig,\n): LocaleContext {\n const strategy = config.strategy ?? 'prefix-except-default'\n\n return {\n locale,\n locales: config.locales,\n defaultLocale: config.defaultLocale,\n\n localePath(targetPath: string, targetLocale?: string) {\n return buildLocalePath(\n targetPath,\n targetLocale ?? locale,\n config.defaultLocale,\n strategy,\n )\n },\n\n alternates() {\n const { pathWithoutLocale } = extractLocaleFromPath(\n path,\n config.locales,\n config.defaultLocale,\n )\n return config.locales.map((loc) => ({\n locale: loc,\n url: buildLocalePath(pathWithoutLocale, loc, config.defaultLocale, strategy),\n }))\n },\n }\n}\n\n/**\n * I18n routing middleware for Zero's server.\n *\n * - Detects locale from URL prefix or Accept-Language header\n * - Redirects root to preferred locale (when detectLocale is true)\n * - Sets locale context for loaders and components\n *\n * @example\n * ```ts\n * // zero.config.ts\n * import { i18nRouting } from \"@pyreon/zero\"\n *\n * export default defineConfig({\n * plugins: [\n * i18nRouting({\n * locales: [\"en\", \"de\", \"cs\"],\n * defaultLocale: \"en\",\n * }),\n * ],\n * })\n * ```\n */\nexport function i18nRouting(config: I18nRoutingConfig): Plugin {\n const strategy = config.strategy ?? 'prefix-except-default'\n const detectEnabled = config.detectLocale !== false\n const cookieName = config.cookieName ?? 'locale'\n\n return {\n name: 'pyreon-zero-i18n-routing',\n\n // Route duplication is NOT handled here. The fs-router's `scanRouteFiles`\n // consumes the i18n config to duplicate routes per locale at build time.\n // This plugin only provides: (1) the server middleware for locale detection\n // and (2) the runtime hooks (useLocale, setLocale) for client-side use.\n configResolved() {},\n\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const url = req.url ?? '/'\n\n // Skip static assets\n if (url.startsWith('/@') || url.startsWith('/__') || url.includes('.')) {\n return next()\n }\n\n const { locale } = extractLocaleFromPath(\n url,\n config.locales,\n config.defaultLocale,\n )\n\n // Redirect root to detected locale\n if (detectEnabled && url === '/') {\n const cookies = parseCookies(req.headers.cookie)\n const preferredFromCookie = cookies[cookieName]\n const preferredFromHeader = detectLocaleFromHeader(\n req.headers['accept-language'],\n config.locales,\n config.defaultLocale,\n )\n const preferred = preferredFromCookie && config.locales.includes(preferredFromCookie)\n ? preferredFromCookie\n : preferredFromHeader\n\n if (strategy === 'prefix' || preferred !== config.defaultLocale) {\n res.writeHead(302, { Location: `/${preferred}/` })\n res.end()\n return\n }\n }\n\n // Attach locale context to request for loaders\n ;(req as any).__locale = locale\n ;(req as any).__localeContext = createLocaleContext(locale, url, config)\n\n // Update the module-level signal so useLocale() returns the correct value\n localeSignal.set(locale)\n\n next()\n })\n },\n }\n}\n\nfunction parseCookies(header: string | undefined): Record<string, string> {\n if (!header) return {}\n const result: Record<string, string> = {}\n for (const pair of header.split(';')) {\n const [key, value] = pair.trim().split('=')\n if (key && value) result[key] = decodeURIComponent(value)\n }\n return result\n}\n\n// ─── Reactive locale hook ───────────────────────────────────────────────────\n\n/** @internal Context for the current locale. */\nexport const LocaleCtx = createContext<string>('en')\n\n/** Current locale signal — set by the server middleware or client-side detection. */\nexport const localeSignal = signal('en')\n\n/**\n * Read the current locale reactively.\n *\n * Returns the locale signal value directly — reactive in both SSR and CSR.\n * The server middleware sets `localeSignal` per-request, and client-side\n * `setLocale()` updates it as well.\n *\n * @example\n * ```tsx\n * const locale = useLocale() // \"en\", \"de\", etc.\n * ```\n */\nexport function useLocale(): string {\n return localeSignal()\n}\n\n/**\n * Set the locale client-side and update the URL.\n *\n * @example\n * ```tsx\n * <button onClick={() => setLocale('de')}>Deutsch</button>\n * ```\n */\nexport function setLocale(\n locale: string,\n config: I18nRoutingConfig,\n): void {\n localeSignal.set(locale)\n\n // Persist to cookie\n if (typeof document !== 'undefined') {\n document.cookie = `${config.cookieName ?? 'locale'}=${locale}; path=/; max-age=31536000`\n }\n\n // Navigate to localized URL — use pushState to avoid full page reload\n if (typeof window !== 'undefined') {\n const strategy = config.strategy ?? 'prefix-except-default'\n const { pathWithoutLocale } = extractLocaleFromPath(\n window.location.pathname,\n config.locales,\n config.defaultLocale,\n )\n const newPath = buildLocalePath(pathWithoutLocale, locale, config.defaultLocale, strategy)\n window.history.pushState(null, '', newPath)\n // Dispatch popstate so @pyreon/router picks up the URL change\n window.dispatchEvent(new PopStateEvent('popstate'))\n }\n}\n","import type { VNodeChild } from '@pyreon/core'\nimport type { UseHeadInput } from '@pyreon/head'\nimport { useHead } from '@pyreon/head'\nimport type { I18nRoutingConfig } from './i18n-routing'\nimport { extractLocaleFromPath } from './i18n-routing'\n\n// ─── Inline helpers (no node:fs dependency) ─────────────────────────────────\n// These are inlined to avoid importing from favicon.ts/og-image.ts which\n// pull in node:fs at the top level — making Meta unsafe for client bundles.\n\n/** Favicon plugin config shape (type-only). */\ninterface FaviconPluginConfig {\n source: string\n themeColor?: string\n manifest?: boolean\n locales?: Record<string, { source: string; darkSource?: string }>\n [key: string]: unknown\n}\n\nfunction faviconLinks(\n locale: string | undefined,\n config: FaviconPluginConfig,\n): Array<{ rel: string; type?: string; sizes?: string; href: string }> {\n const hasLocaleOverride = locale && config.locales?.[locale]\n const prefix = hasLocaleOverride ? `/${locale}` : ''\n const isSvg = (hasLocaleOverride ? config.locales![locale]!.source : config.source).endsWith('.svg')\n const links: Array<{ rel: string; type?: string; sizes?: string; href: string }> = []\n if (isSvg) links.push({ rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon.svg` })\n links.push(\n { rel: 'icon', type: 'image/png', sizes: '32x32', href: `${prefix}/favicon-32x32.png` },\n { rel: 'icon', type: 'image/png', sizes: '16x16', href: `${prefix}/favicon-16x16.png` },\n { rel: 'apple-touch-icon', sizes: '180x180', href: `${prefix}/apple-touch-icon.png` },\n )\n if (config.manifest !== false) links.push({ rel: 'manifest', href: `${prefix}/site.webmanifest` })\n return links\n}\n\nfunction ogImagePath(templateName: string, locale?: string, outDir = 'og', format: 'png' | 'jpeg' = 'png'): string {\n const ext = format === 'jpeg' ? 'jpg' : 'png'\n const suffix = locale ? `-${locale}` : ''\n return `/${outDir}/${templateName}${suffix}.${ext}`\n}\n\n// ─── Meta component ────────────────────────────────────────────────────────\n\nexport interface MetaProps {\n /** Page title. Accepts reactive accessor `() => string`. */\n title?: string | (() => string)\n /** Page description. Accepts reactive accessor. */\n description?: string | (() => string)\n /** Canonical URL. Also sets og:url. */\n canonical?: string\n /** Open Graph image URL. Also sets twitter:image. */\n image?: string\n /** Image alt text for accessibility. */\n imageAlt?: string\n /** Image width in pixels (og:image:width). Helps crawlers layout before loading. */\n imageWidth?: number\n /** Image height in pixels (og:image:height). */\n imageHeight?: number\n /** Open Graph type. Default: \"website\" */\n type?: 'website' | 'article' | 'product' | 'profile'\n /** Site name for og:site_name. */\n siteName?: string\n /** Twitter card type. Default: \"summary_large_image\" */\n twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player'\n /** Twitter @handle. */\n twitterSite?: string\n /** Twitter creator @handle. */\n twitterCreator?: string\n /** Locale. Default: \"en_US\" */\n locale?: string\n /** Alternate locales for hreflang. */\n alternateLocales?: Array<{ locale: string; url: string }>\n /** Robots directives. Default: \"index, follow\" */\n robots?: string\n /** Convenience: set `true` to emit `noindex, nofollow`. Overrides `robots`. */\n noIndex?: boolean\n /** Published time (ISO 8601) for article type. */\n publishedTime?: string\n /** Modified time (ISO 8601) for article type. */\n modifiedTime?: string\n /** Article author. */\n author?: string\n /** Article tags. */\n tags?: string[]\n /** JSON-LD structured data object. */\n jsonLd?: Record<string, unknown>\n /** Additional custom meta tags. */\n extra?: Array<{ name?: string; property?: string; content: string }>\n /**\n * Open Graph video URL. Also sets og:video:type if the URL ends with\n * a known extension (.mp4, .webm).\n */\n video?: string\n /** Video width in pixels. */\n videoWidth?: number\n /** Video height in pixels. */\n videoHeight?: number\n /**\n * Open Graph audio URL.\n */\n audio?: string\n /**\n * I18n routing config — when provided, auto-generates hreflang alternate\n * links for all locales based on the current path.\n * Also sets og:locale and og:locale:alternate.\n */\n i18n?: I18nRoutingConfig\n /** Base URL for building absolute hreflang URLs. e.g. \"https://example.com\" */\n origin?: string\n /**\n * Favicon plugin config — when provided, injects locale-aware favicon\n * `<link>` tags into `<head>`. Uses the current locale to select\n * the correct favicon set.\n */\n favicon?: FaviconPluginConfig\n /**\n * OG image template name — auto-resolves to the correct locale-specific\n * OG image path generated by `ogImagePlugin`.\n * Sets both `og:image` and `twitter:image` unless `image` is also provided.\n */\n ogTemplate?: string\n /** Output directory for OG images. Default: \"og\" */\n ogImageDir?: string\n /** OG image format. Default: \"png\" */\n ogImageFormat?: 'png' | 'jpeg'\n children?: VNodeChild\n}\n\nconst resolveStr = (v: string | (() => string) | undefined): string | undefined =>\n typeof v === 'function' ? v() : v\n\n/**\n * Declarative meta component for SSR-compatible page metadata.\n *\n * Supports reactive title/description — when passed as `() => string` accessors,\n * they are forwarded to `useHead()` as a reactive getter so updates propagate\n * automatically via signal tracking.\n *\n * @example\n * ```tsx\n * <Meta title=\"My Page\" description=\"...\" image=\"/og.jpg\" canonical=\"https://...\" />\n * ```\n *\n * @example Reactive title\n * ```tsx\n * const count = signal(0)\n * <Meta title={() => `${count()} items`} />\n * ```\n */\nexport function Meta(props: MetaProps): VNodeChild {\n const hasReactiveTitle = typeof props.title === 'function'\n const hasReactiveDescription = typeof props.description === 'function'\n\n // If title or description are reactive accessors, pass a getter to useHead\n // so it re-evaluates when the signals change.\n if (hasReactiveTitle || hasReactiveDescription) {\n useHead((): UseHeadInput => {\n const title = resolveStr(props.title)\n const description = resolveStr(props.description)\n const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]\n const tags = buildMetaTags(resolved)\n const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }\n if (title) input.title = title\n return input\n })\n } else {\n const title = resolveStr(props.title)\n const description = resolveStr(props.description)\n const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]\n const tags = buildMetaTags(resolved)\n const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }\n if (title) input.title = title\n useHead(input)\n }\n\n return props.children ?? null\n}\n\ninterface MetaTagEntry {\n name?: string\n property?: string\n content: string\n [key: string]: string | undefined\n}\n\ninterface LinkTagEntry {\n rel: string\n href?: string\n hreflang?: string\n type?: string\n sizes?: string\n [key: string]: string | undefined\n}\n\ninterface ScriptTagEntry {\n type: string\n children: string\n}\n\ninterface MetaTags {\n meta: MetaTagEntry[]\n link: LinkTagEntry[]\n script: ScriptTagEntry[]\n}\n\nexport function buildMetaTags(\n props: Omit<MetaProps, 'title' | 'description' | 'children'> & {\n title?: string\n description?: string\n },\n): MetaTags {\n const meta: MetaTagEntry[] = []\n const link: LinkTagEntry[] = []\n const script: ScriptTagEntry[] = []\n\n const {\n title, description, canonical, imageAlt, imageWidth, imageHeight,\n type = 'website', siteName,\n twitterCard = 'summary_large_image', twitterSite, twitterCreator,\n locale = 'en_US', alternateLocales,\n publishedTime, modifiedTime, author, tags, jsonLd, extra,\n video, videoWidth, videoHeight, audio,\n favicon, ogTemplate, ogImageDir, ogImageFormat,\n } = props\n\n // noIndex convenience overrides robots\n const robots = props.noIndex ? 'noindex, nofollow' : (props.robots ?? 'index, follow')\n\n // Resolve image: explicit `image` prop takes precedence over `ogTemplate`\n const image = props.image ?? (\n ogTemplate\n ? ogImagePath(ogTemplate, locale !== 'en_US' ? locale : undefined, ogImageDir, ogImageFormat)\n : undefined\n )\n\n // Auto-resolve image dimensions for OG template images\n const resolvedImageWidth = imageWidth ?? (ogTemplate && !props.image ? 1200 : undefined)\n const resolvedImageHeight = imageHeight ?? (ogTemplate && !props.image ? 630 : undefined)\n\n if (description) meta.push({ name: 'description', content: description })\n if (robots) meta.push({ name: 'robots', content: robots })\n if (author) meta.push({ name: 'author', content: author })\n\n if (title) meta.push({ property: 'og:title', content: title })\n if (description) meta.push({ property: 'og:description', content: description })\n if (canonical) meta.push({ property: 'og:url', content: canonical })\n if (image) meta.push({ property: 'og:image', content: image })\n if (imageAlt) meta.push({ property: 'og:image:alt', content: imageAlt })\n if (resolvedImageWidth) meta.push({ property: 'og:image:width', content: String(resolvedImageWidth) })\n if (resolvedImageHeight) meta.push({ property: 'og:image:height', content: String(resolvedImageHeight) })\n meta.push({ property: 'og:type', content: type })\n if (siteName) meta.push({ property: 'og:site_name', content: siteName })\n meta.push({ property: 'og:locale', content: locale })\n\n // Video\n if (video) {\n meta.push({ property: 'og:video', content: video })\n if (videoWidth) meta.push({ property: 'og:video:width', content: String(videoWidth) })\n if (videoHeight) meta.push({ property: 'og:video:height', content: String(videoHeight) })\n // Auto-detect video type from extension\n if (video.endsWith('.mp4')) meta.push({ property: 'og:video:type', content: 'video/mp4' })\n else if (video.endsWith('.webm')) meta.push({ property: 'og:video:type', content: 'video/webm' })\n }\n\n // Audio\n if (audio) {\n meta.push({ property: 'og:audio', content: audio })\n }\n\n if (type === 'article') {\n if (publishedTime) meta.push({ property: 'article:published_time', content: publishedTime })\n if (modifiedTime) meta.push({ property: 'article:modified_time', content: modifiedTime })\n if (author) meta.push({ property: 'article:author', content: author })\n if (tags) for (const tag of tags) meta.push({ property: 'article:tag', content: tag })\n }\n\n meta.push({ name: 'twitter:card', content: twitterCard })\n if (title) meta.push({ name: 'twitter:title', content: title })\n if (description) meta.push({ name: 'twitter:description', content: description })\n if (image) meta.push({ name: 'twitter:image', content: image })\n if (imageAlt) meta.push({ name: 'twitter:image:alt', content: imageAlt })\n if (twitterSite) meta.push({ name: 'twitter:site', content: twitterSite })\n if (twitterCreator) meta.push({ name: 'twitter:creator', content: twitterCreator })\n\n if (canonical) link.push({ rel: 'canonical', href: canonical })\n if (alternateLocales) {\n for (const alt of alternateLocales) {\n link.push({ rel: 'alternate', hreflang: alt.locale, href: alt.url })\n }\n }\n\n if (jsonLd) {\n script.push({\n type: 'application/ld+json',\n children: JSON.stringify({ '@context': 'https://schema.org', ...jsonLd }),\n })\n }\n\n if (extra) for (const tag of extra) meta.push(tag)\n\n // I18n: auto-generate hreflang alternates from i18nRouting config\n if (props.i18n) {\n const i18nConfig = props.i18n\n const origin = props.origin ?? ''\n const currentPath = canonical?.replace(origin, '') ?? '/'\n const { pathWithoutLocale } = extractLocaleFromPath(\n currentPath,\n i18nConfig.locales,\n i18nConfig.defaultLocale,\n )\n const strategy = i18nConfig.strategy ?? 'prefix-except-default'\n\n for (const loc of i18nConfig.locales) {\n const localizedPath =\n strategy === 'prefix-except-default' && loc === i18nConfig.defaultLocale\n ? pathWithoutLocale\n : `/${loc}${pathWithoutLocale === '/' ? '' : pathWithoutLocale}`\n\n link.push({\n rel: 'alternate',\n hreflang: loc,\n href: `${origin}${localizedPath}`,\n })\n\n // og:locale:alternate for non-current locales\n if (loc !== locale) {\n meta.push({ property: 'og:locale:alternate', content: loc })\n }\n }\n\n // x-default hreflang pointing to default locale\n link.push({\n rel: 'alternate',\n hreflang: 'x-default',\n href: `${origin}${pathWithoutLocale}`,\n })\n }\n\n // Favicon: inject locale-aware favicon links\n if (favicon) {\n const faviconLocale = locale !== 'en_US' ? locale : undefined\n for (const fl of faviconLinks(faviconLocale, favicon)) {\n link.push(fl as LinkTagEntry)\n }\n // Theme color meta from favicon config\n if (favicon.themeColor) {\n meta.push({ name: 'theme-color', content: favicon.themeColor })\n }\n }\n\n return { meta, link, script }\n}\n","import type { VNodeChild } from '@pyreon/core'\nimport { onMount, onUnmount } from '@pyreon/core'\nimport { effect, signal } from '@pyreon/reactivity'\n\n// ─── Theme system ───────────────────────────────────────────────────────────\n//\n// Provides dark/light/system theme support with:\n// - System preference detection via matchMedia\n// - Persistent preference via localStorage\n// - No flash of wrong theme (inline script in HTML)\n// - Reactive theme signal for components\n\nexport type Theme = 'light' | 'dark' | 'system'\n\nconst STORAGE_KEY = 'zero-theme'\n\n/** Reactive theme signal. */\nexport const theme = signal<Theme>('system')\n\n/** SSR fallback when system preference can't be detected. Default: 'light'. */\nlet _ssrDefault: 'light' | 'dark' = 'light'\n\n/**\n * Set the default theme for SSR (when `matchMedia` is unavailable).\n * Call once at server startup before rendering.\n */\nexport function setSSRThemeDefault(value: 'light' | 'dark'): void {\n _ssrDefault = value\n}\n\n/** Computed resolved theme (what's actually applied). */\nexport function resolvedTheme(): 'light' | 'dark' {\n const t = theme()\n if (t === 'system') {\n if (typeof window === 'undefined') return _ssrDefault\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n }\n return t\n}\n\n/** Toggle between light and dark. */\nexport function toggleTheme() {\n const current = resolvedTheme()\n setTheme(current === 'dark' ? 'light' : 'dark')\n}\n\n/** Set theme explicitly. */\nexport function setTheme(t: Theme) {\n theme.set(t)\n if (typeof document !== 'undefined') {\n document.documentElement.dataset.theme = resolvedTheme()\n try {\n localStorage.setItem(STORAGE_KEY, t)\n } catch {\n // localStorage may not be available (SSR, private browsing)\n }\n }\n}\n\n/**\n * Initialize the theme system. Call once in your app entry or layout.\n * Reads from localStorage, listens for system preference changes.\n */\nexport function initTheme() {\n onMount(() => {\n // Read persisted preference\n try {\n const stored = localStorage.getItem(STORAGE_KEY) as Theme | null\n if (stored === 'light' || stored === 'dark' || stored === 'system') {\n theme.set(stored)\n }\n } catch {\n // localStorage may not be available\n }\n\n // Apply to document\n document.documentElement.dataset.theme = resolvedTheme()\n\n // Watch for system preference changes\n const mq = window.matchMedia('(prefers-color-scheme: dark)')\n function onChange() {\n if (theme() === 'system') {\n document.documentElement.dataset.theme = resolvedTheme()\n }\n }\n mq.addEventListener('change', onChange)\n onUnmount(() => mq.removeEventListener('change', onChange))\n\n // Re-apply when theme signal changes — updates data-theme + favicons\n const dispose = effect(() => {\n const mode = resolvedTheme()\n document.documentElement.dataset.theme = mode\n\n // Swap favicon variants (if dual-variant favicons are present)\n const faviconLinks = document.querySelectorAll<HTMLLinkElement>('[data-favicon-theme]')\n for (const link of faviconLinks) {\n link.media = link.dataset.faviconTheme === mode ? '' : 'not all'\n }\n })\n if (dispose) onUnmount(() => dispose.dispose())\n\n return undefined\n })\n}\n\n/**\n * Theme toggle button component.\n *\n * @example\n * import { ThemeToggle } from \"@pyreon/zero/theme\"\n * <ThemeToggle />\n */\nexport function ThemeToggle(props: { class?: string; style?: string }): VNodeChild {\n initTheme()\n\n return (\n <button\n class={props.class}\n style={props.style}\n onClick={toggleTheme}\n aria-label=\"Toggle theme\"\n title=\"Toggle theme\"\n type=\"button\"\n >\n {() =>\n resolvedTheme() === 'dark' ? (\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"5\" />\n <line x1=\"12\" y1=\"1\" x2=\"12\" y2=\"3\" />\n <line x1=\"12\" y1=\"21\" x2=\"12\" y2=\"23\" />\n <line x1=\"4.22\" y1=\"4.22\" x2=\"5.64\" y2=\"5.64\" />\n <line x1=\"18.36\" y1=\"18.36\" x2=\"19.78\" y2=\"19.78\" />\n <line x1=\"1\" y1=\"12\" x2=\"3\" y2=\"12\" />\n <line x1=\"21\" y1=\"12\" x2=\"23\" y2=\"12\" />\n <line x1=\"4.22\" y1=\"19.78\" x2=\"5.64\" y2=\"18.36\" />\n <line x1=\"18.36\" y1=\"5.64\" x2=\"19.78\" y2=\"4.22\" />\n </svg>\n ) : (\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\" />\n </svg>\n )\n }\n </button>\n )\n}\n\n/**\n * Inline script to prevent flash of wrong theme.\n * Include this in your index.html <head> BEFORE any stylesheets.\n *\n * @example\n * // index.html\n * <head>\n * <script>{themeScript}</script>\n * ...\n * </head>\n */\nexport const themeScript = `(function(){try{var t=localStorage.getItem(\"${STORAGE_KEY}\");var r=t===\"light\"?\"light\":t===\"dark\"?\"dark\":window.matchMedia(\"(prefers-color-scheme:dark)\").matches?\"dark\":\"light\";document.documentElement.dataset.theme=r;document.querySelectorAll(\"[data-favicon-theme]\").forEach(function(l){l.media=l.dataset.faviconTheme===r?\"\":\"not all\"})}catch(e){}})()`\n","/**\n * @pyreon/zero — client-safe exports.\n *\n * This entry contains only browser-safe components and hooks.\n * No node:fs, node:path, or other server-only imports.\n *\n * For server/build-time features, use subpath imports:\n * import { faviconPlugin } from \"@pyreon/zero/favicon\"\n * import { createServer } from \"@pyreon/zero/server\"\n * import { defineConfig } from \"@pyreon/zero/config\"\n * import { validateEnv } from \"@pyreon/zero/env\"\n */\n\n// ─── Components (browser-safe) ──────────────────────────────────────────────\n\nexport type { ImageProps, ImageSource } from \"./image\";\nexport { Image } from \"./image\";\nexport type { LinkProps, LinkRenderProps, UseLinkReturn } from \"./link\";\nexport { createLink, Link, prefetchRoute, useLink } from \"./link\";\nexport type { ScriptProps, ScriptStrategy } from \"./script\";\nexport { Script } from \"./script\";\nexport type { MetaProps } from \"./meta\";\nexport { buildMetaTags, Meta } from \"./meta\";\n\n// ─── Theme (browser-safe) ───────────────────────────────────────────────────\n\nexport type { Theme } from \"./theme\";\nexport {\n\tinitTheme,\n\tresolvedTheme,\n\tsetSSRThemeDefault,\n\tsetTheme,\n\tThemeToggle,\n\ttheme,\n\tthemeScript,\n\ttoggleTheme,\n} from \"./theme\";\n\n// ─── I18n hooks (browser-safe) ──────────────────────────────────────────────\n\nexport type { I18nRoutingConfig, LocaleContext } from \"./i18n-routing\";\nexport {\n\tbuildLocalePath,\n\textractLocaleFromPath,\n\tsetLocale,\n\tuseLocale,\n} from \"./i18n-routing\";\n\n// ─── Server-only stubs ──────────────────────────────────────────────────────\n// Throw clear error messages when developers accidentally import server-only\n// APIs from the main entry. These are tree-shaken if not imported.\n\nfunction serverOnly(name: string, subpath: string): never {\n throw new Error(\n `[Pyreon] \"${name}\" is server-only and cannot be imported from \"@pyreon/zero\".\\n` +\n `Import from the subpath instead:\\n\\n` +\n ` import { ${name} } from \"@pyreon/zero/${subpath}\"\\n`,\n )\n}\n\n/* eslint-disable @typescript-eslint/no-unused-vars */\n/** @deprecated Import from `@pyreon/zero/favicon` instead */\nexport function faviconPlugin(..._: unknown[]): never { return serverOnly('faviconPlugin', 'favicon') }\n/** @deprecated Import from `@pyreon/zero/seo` instead */\nexport function seoPlugin(..._: unknown[]): never { return serverOnly('seoPlugin', 'seo') }\n/** @deprecated Import from `@pyreon/zero/server` instead */\nexport function createServer(..._: unknown[]): never { return serverOnly('createServer', 'server') }\n/** @deprecated Import from `@pyreon/zero/config` instead */\nexport function defineConfig(..._: unknown[]): never { return serverOnly('defineConfig', 'config') }\n/** @deprecated Import from `@pyreon/zero/env` instead */\nexport function validateEnv(..._: unknown[]): never { return serverOnly('validateEnv', 'env') }\n/** @deprecated Import from `@pyreon/zero/og-image` instead */\nexport function ogImagePlugin(..._: unknown[]): never { return serverOnly('ogImagePlugin', 'og-image') }\n/** @deprecated Import from `@pyreon/zero/ai` instead */\nexport function aiPlugin(..._: unknown[]): never { return serverOnly('aiPlugin', 'ai') }\n\n// ─── Types (no runtime, safe everywhere) ────────────────────────────────────\n\nexport type {\n\tAdapter,\n\tAdapterBuildOptions,\n\tFileRoute,\n\tISRConfig,\n\tLoaderContext,\n\tRenderMode,\n\tRouteMeta,\n\tRouteMiddlewareEntry,\n\tRouteModule,\n\tZeroConfig,\n} from \"./types\";\n"],"mappings":"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"}