@pyreon/zero 0.4.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/lib/cache.js.map +1 -1
  2. package/lib/client.js.map +1 -1
  3. package/lib/config.js.map +1 -1
  4. package/lib/font.js.map +1 -1
  5. package/lib/fs-router-BkbIWqek.js.map +1 -1
  6. package/lib/fs-router-n4VA4lxu.js.map +1 -1
  7. package/lib/image-plugin.js.map +1 -1
  8. package/lib/image.js +5 -5
  9. package/lib/image.js.map +1 -1
  10. package/lib/index.js +14 -14
  11. package/lib/index.js.map +1 -1
  12. package/lib/link.js +9 -9
  13. package/lib/link.js.map +1 -1
  14. package/lib/script.js +1 -1
  15. package/lib/script.js.map +1 -1
  16. package/lib/seo.js.map +1 -1
  17. package/lib/theme.js +2 -2
  18. package/lib/theme.js.map +1 -1
  19. package/package.json +14 -13
  20. package/src/actions.ts +20 -28
  21. package/src/adapters/bun.ts +7 -7
  22. package/src/adapters/index.ts +12 -14
  23. package/src/adapters/node.ts +8 -11
  24. package/src/adapters/static.ts +3 -3
  25. package/src/api-routes.ts +23 -50
  26. package/src/app.ts +9 -13
  27. package/src/cache.ts +16 -29
  28. package/src/client.ts +8 -8
  29. package/src/compression.ts +21 -28
  30. package/src/config.ts +6 -7
  31. package/src/cors.ts +20 -28
  32. package/src/entry-server.ts +15 -19
  33. package/src/error-overlay.ts +10 -13
  34. package/src/font.ts +44 -55
  35. package/src/fs-router.ts +44 -63
  36. package/src/image-plugin.ts +53 -79
  37. package/src/image.tsx +41 -43
  38. package/src/index.ts +36 -36
  39. package/src/isr.ts +8 -8
  40. package/src/link.tsx +35 -38
  41. package/src/rate-limit.ts +15 -15
  42. package/src/script.tsx +21 -22
  43. package/src/seo.ts +47 -57
  44. package/src/sharp.d.ts +2 -6
  45. package/src/testing.ts +8 -12
  46. package/src/theme.tsx +19 -21
  47. package/src/types.ts +6 -6
  48. package/src/utils/use-intersection-observer.ts +2 -2
  49. package/src/utils/with-headers.ts +1 -4
  50. package/src/vite-plugin.ts +21 -28
  51. package/lib/types/actions.d.ts +0 -57
  52. package/lib/types/actions.d.ts.map +0 -1
  53. package/lib/types/adapters/bun.d.ts +0 -6
  54. package/lib/types/adapters/bun.d.ts.map +0 -1
  55. package/lib/types/adapters/index.d.ts +0 -10
  56. package/lib/types/adapters/index.d.ts.map +0 -1
  57. package/lib/types/adapters/node.d.ts +0 -6
  58. package/lib/types/adapters/node.d.ts.map +0 -1
  59. package/lib/types/adapters/static.d.ts +0 -7
  60. package/lib/types/adapters/static.d.ts.map +0 -1
  61. package/lib/types/api-routes.d.ts +0 -66
  62. package/lib/types/api-routes.d.ts.map +0 -1
  63. package/lib/types/app.d.ts +0 -24
  64. package/lib/types/app.d.ts.map +0 -1
  65. package/lib/types/cache.d.ts +0 -54
  66. package/lib/types/cache.d.ts.map +0 -1
  67. package/lib/types/client.d.ts +0 -19
  68. package/lib/types/client.d.ts.map +0 -1
  69. package/lib/types/compression.d.ts +0 -33
  70. package/lib/types/compression.d.ts.map +0 -1
  71. package/lib/types/config.d.ts +0 -18
  72. package/lib/types/config.d.ts.map +0 -1
  73. package/lib/types/cors.d.ts +0 -32
  74. package/lib/types/cors.d.ts.map +0 -1
  75. package/lib/types/entry-server.d.ts +0 -34
  76. package/lib/types/entry-server.d.ts.map +0 -1
  77. package/lib/types/error-overlay.d.ts +0 -6
  78. package/lib/types/error-overlay.d.ts.map +0 -1
  79. package/lib/types/font.d.ts +0 -119
  80. package/lib/types/font.d.ts.map +0 -1
  81. package/lib/types/fs-router.d.ts +0 -38
  82. package/lib/types/fs-router.d.ts.map +0 -1
  83. package/lib/types/image-plugin.d.ts +0 -79
  84. package/lib/types/image-plugin.d.ts.map +0 -1
  85. package/lib/types/image.d.ts +0 -51
  86. package/lib/types/image.d.ts.map +0 -1
  87. package/lib/types/index.d.ts +0 -37
  88. package/lib/types/index.d.ts.map +0 -1
  89. package/lib/types/isr.d.ts +0 -9
  90. package/lib/types/isr.d.ts.map +0 -1
  91. package/lib/types/link.d.ts +0 -116
  92. package/lib/types/link.d.ts.map +0 -1
  93. package/lib/types/rate-limit.d.ts +0 -34
  94. package/lib/types/rate-limit.d.ts.map +0 -1
  95. package/lib/types/script.d.ts +0 -35
  96. package/lib/types/script.d.ts.map +0 -1
  97. package/lib/types/seo.d.ts +0 -88
  98. package/lib/types/seo.d.ts.map +0 -1
  99. package/lib/types/testing.d.ts +0 -85
  100. package/lib/types/testing.d.ts.map +0 -1
  101. package/lib/types/theme.d.ts +0 -39
  102. package/lib/types/theme.d.ts.map +0 -1
  103. package/lib/types/types.d.ts +0 -109
  104. package/lib/types/types.d.ts.map +0 -1
  105. package/lib/types/utils/use-intersection-observer.d.ts +0 -10
  106. package/lib/types/utils/use-intersection-observer.d.ts.map +0 -1
  107. package/lib/types/utils/with-headers.d.ts +0 -6
  108. package/lib/types/utils/with-headers.d.ts.map +0 -1
  109. package/lib/types/vite-plugin.d.ts +0 -17
  110. package/lib/types/vite-plugin.d.ts.map +0 -1
package/lib/link.js CHANGED
@@ -26,7 +26,7 @@ function useIntersectionObserver(getElement, onIntersect, rootMargin = "200px")
26
26
  }
27
27
 
28
28
  //#endregion
29
- //#region ../../node_modules/.bun/@pyreon+core@0.7.5/node_modules/@pyreon/core/lib/jsx-runtime.js
29
+ //#region ../../core/core/lib/jsx-runtime.js
30
30
  /**
31
31
  * Hyperscript function — the compiled output of JSX.
32
32
  * `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
@@ -102,7 +102,7 @@ function doPrefetch(href) {
102
102
  * function MyLink(props: LinkProps) {
103
103
  * const link = useLink(props)
104
104
  * return (
105
- * <button ref={link.ref} class={link.classes()} onclick={link.handleClick}>
105
+ * <button ref={link.ref} class={link.classes()} onClick={link.handleClick}>
106
106
  * {props.children}
107
107
  * </button>
108
108
  * )
@@ -164,8 +164,8 @@ function useLink(props) {
164
164
  * <button
165
165
  * ref={props.ref}
166
166
  * class={props.class}
167
- * onclick={props.onClick}
168
- * onmouseenter={props.onMouseEnter}
167
+ * onClick={props.onClick}
168
+ * onMouseEnter={props.onMouseEnter}
169
169
  * >
170
170
  * {props.children}
171
171
  * </button>
@@ -176,8 +176,8 @@ function useLink(props) {
176
176
  * <div
177
177
  * ref={props.ref}
178
178
  * class={`card ${props.isActive() ? "card--active" : ""}`}
179
- * onclick={props.onClick}
180
- * onmouseenter={props.onMouseEnter}
179
+ * onClick={props.onClick}
180
+ * onMouseEnter={props.onMouseEnter}
181
181
  * >
182
182
  * {props.children}
183
183
  * </div>
@@ -225,9 +225,9 @@ const Link = createLink((props) => /* @__PURE__ */ jsx("a", {
225
225
  ...props.rel ? { rel: props.rel } : {},
226
226
  ...props["aria-label"] ? { "aria-label": props["aria-label"] } : {},
227
227
  ...props.isExactActive() ? { "aria-current": "page" } : {},
228
- onclick: props.onClick,
229
- onmouseenter: props.onMouseEnter,
230
- ontouchstart: props.onTouchStart,
228
+ onClick: props.onClick,
229
+ onMouseEnter: props.onMouseEnter,
230
+ onTouchStart: props.onTouchStart,
231
231
  children: props.children
232
232
  }));
233
233
 
package/lib/link.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"link.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../../../node_modules/.bun/@pyreon+core@0.7.5/node_modules/@pyreon/core/lib/jsx-runtime.js","../src/link.tsx"],"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 { 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}\n\n/** Props passed to a custom component via createLink. */\nexport interface LinkRenderProps {\n href: string\n ref: import('@pyreon/core').Ref<HTMLElement>\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<HTMLElement>\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 prefetched = new Set<string>()\n\nfunction doPrefetch(href: string) {\n if (prefetched.has(href)) return\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 * 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<HTMLElement>()\n const strategy = props.prefetch ?? 'hover'\n\n function handleClick(e: MouseEvent) {\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 ) {\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())\n 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(\n Component: (props: LinkRenderProps) => any,\n): (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 as any}\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"],"x_google_ignoreList":[1],"mappings":"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"}
1
+ {"version":3,"file":"link.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../../../core/core/lib/jsx-runtime.js","../src/link.tsx"],"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 { 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}\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 prefetched = new Set<string>()\n\nfunction doPrefetch(href: string) {\n if (prefetched.has(href)) return\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 * 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 if (\n e.defaultPrevented ||\n e.button !== 0 ||\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n e.altKey ||\n props.external\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"],"mappings":"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"}
package/lib/script.js CHANGED
@@ -25,7 +25,7 @@ function useIntersectionObserver(getElement, onIntersect, rootMargin = "200px")
25
25
  }
26
26
 
27
27
  //#endregion
28
- //#region ../../node_modules/.bun/@pyreon+core@0.7.5/node_modules/@pyreon/core/lib/jsx-runtime.js
28
+ //#region ../../core/core/lib/jsx-runtime.js
29
29
  /**
30
30
  * Hyperscript function — the compiled output of JSX.
31
31
  * `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
package/lib/script.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"script.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../../../node_modules/.bun/@pyreon+core@0.7.5/node_modules/@pyreon/core/lib/jsx-runtime.js","../src/script.tsx"],"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, 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 = () =>\n 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"],"x_google_ignoreList":[1],"mappings":"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"}
1
+ {"version":3,"file":"script.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../../../core/core/lib/jsx-runtime.js","../src/script.tsx"],"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, 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"],"mappings":"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"}
package/lib/seo.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"seo.js","names":[],"sources":["../src/seo.ts"],"sourcesContent":["import type { Middleware } from '@pyreon/server'\nimport type { Plugin } from 'vite'\n\n// ─── SEO utilities ──────────────────────────────────────────────────────────\n//\n// Zero provides built-in SEO tooling:\n// - Automatic sitemap.xml generation from file-based routes\n// - Configurable robots.txt\n// - Structured data (JSON-LD) helpers\n// - Open Graph / Twitter Card meta helpers\n\nexport interface SitemapConfig {\n /** Base URL of the site (required). e.g. \"https://example.com\" */\n origin: string\n /** Paths to exclude from the sitemap. */\n exclude?: string[]\n /** Default change frequency. Default: \"weekly\" */\n changefreq?: ChangeFreq\n /** Default priority. Default: 0.7 */\n priority?: number\n /** Additional URLs to include (for dynamic routes). */\n additionalPaths?: SitemapEntry[]\n}\n\nexport interface SitemapEntry {\n path: string\n changefreq?: ChangeFreq\n priority?: number\n lastmod?: string\n}\n\nexport type ChangeFreq =\n | 'always'\n | 'hourly'\n | 'daily'\n | 'weekly'\n | 'monthly'\n | 'yearly'\n | 'never'\n\n/**\n * Generate a sitemap.xml string from route file paths.\n */\nexport function generateSitemap(\n routeFiles: string[],\n config: SitemapConfig,\n): string {\n const { origin, exclude = [], changefreq = 'weekly', priority = 0.7 } = config\n\n const paths = routeFiles\n .filter((f) => {\n // Exclude layout, error, loading files\n const name = f\n .split('/')\n .pop()\n ?.replace(/\\.\\w+$/, '')\n return name !== '_layout' && name !== '_error' && name !== '_loading'\n })\n .map((f) => {\n // Convert file path to URL\n let path = f\n .replace(/\\.\\w+$/, '')\n .replace(/\\/index$/, '/')\n .replace(/^index$/, '/')\n\n // Skip dynamic routes — they need additionalPaths\n if (path.includes('[')) return null\n\n // Strip route groups\n path = path.replace(/\\([\\w-]+\\)\\//g, '')\n\n if (!path.startsWith('/')) path = `/${path}`\n return path\n })\n .filter((p): p is string => p !== null)\n .filter((p) => !exclude.some((e) => p.startsWith(e)))\n\n const allPaths: SitemapEntry[] = [\n ...paths.map((p) => ({ path: p, changefreq, priority })),\n ...(config.additionalPaths ?? []),\n ]\n\n const entries = allPaths\n .map((entry) => {\n const loc = `${origin}${entry.path === '/' ? '' : entry.path}`\n return ` <url>\n <loc>${escapeXml(loc)}</loc>\n <changefreq>${entry.changefreq ?? changefreq}</changefreq>\n <priority>${entry.priority ?? priority}</priority>${entry.lastmod ? `\\n <lastmod>${entry.lastmod}</lastmod>` : ''}\n </url>`\n })\n .join('\\n')\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${entries}\n</urlset>`\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;')\n}\n\n// ─── Robots.txt ─────────────────────────────────────────────────────────────\n\nexport interface RobotsConfig {\n /** Rules per user-agent. */\n rules?: RobotsRule[]\n /** Sitemap URL. */\n sitemap?: string\n /** Host directive. */\n host?: string\n}\n\nexport interface RobotsRule {\n userAgent: string\n allow?: string[]\n disallow?: string[]\n crawlDelay?: number\n}\n\n/**\n * Generate a robots.txt string.\n */\nexport function generateRobots(config: RobotsConfig = {}): string {\n const { rules = [{ userAgent: '*', allow: ['/'] }], sitemap, host } = config\n const lines: string[] = []\n\n for (const rule of rules) {\n lines.push(`User-agent: ${rule.userAgent}`)\n if (rule.allow) {\n for (const path of rule.allow) lines.push(`Allow: ${path}`)\n }\n if (rule.disallow) {\n for (const path of rule.disallow) lines.push(`Disallow: ${path}`)\n }\n if (rule.crawlDelay) lines.push(`Crawl-delay: ${rule.crawlDelay}`)\n lines.push('')\n }\n\n if (sitemap) lines.push(`Sitemap: ${sitemap}`)\n if (host) lines.push(`Host: ${host}`)\n\n return lines.join('\\n')\n}\n\n// ─── Structured data (JSON-LD) ──────────────────────────────────────────────\n\nexport type JsonLdType =\n | 'WebSite'\n | 'WebPage'\n | 'Article'\n | 'BlogPosting'\n | 'Product'\n | 'Organization'\n | 'Person'\n | 'BreadcrumbList'\n | 'FAQPage'\n | (string & {})\n\n/**\n * Generate a JSON-LD script tag string for structured data.\n *\n * @example\n * useHead({\n * script: [jsonLd({\n * \"@type\": \"WebSite\",\n * name: \"My Site\",\n * url: \"https://example.com\",\n * })],\n * })\n */\nexport function jsonLd(data: Record<string, unknown>): string {\n const ld = {\n '@context': 'https://schema.org',\n ...data,\n }\n return `<script type=\"application/ld+json\">${JSON.stringify(ld)}</script>`\n}\n\n// ─── SEO Vite plugin ────────────────────────────────────────────────────────\n\nexport interface SeoPluginConfig {\n /** Sitemap configuration. */\n sitemap?: SitemapConfig\n /** Robots.txt configuration. */\n robots?: RobotsConfig\n}\n\n/**\n * Zero SEO Vite plugin.\n * Generates sitemap.xml and robots.txt at build time.\n *\n * @example\n * import { seoPlugin } from \"@pyreon/zero/seo\"\n *\n * export default {\n * plugins: [\n * pyreon(),\n * zero(),\n * seoPlugin({\n * sitemap: { origin: \"https://example.com\" },\n * robots: { sitemap: \"https://example.com/sitemap.xml\" },\n * }),\n * ],\n * }\n */\nexport function seoPlugin(config: SeoPluginConfig = {}): Plugin {\n return {\n name: 'pyreon-zero-seo',\n apply: 'build',\n\n async generateBundle(_, _bundle) {\n // Generate sitemap.xml\n if (config.sitemap) {\n const { scanRouteFiles } = await import('./fs-router')\n const routesDir = `${process.cwd()}/src/routes`\n\n try {\n const files = await scanRouteFiles(routesDir)\n const sitemap = generateSitemap(files, config.sitemap)\n\n this.emitFile({\n type: 'asset',\n fileName: 'sitemap.xml',\n source: sitemap,\n })\n } catch {\n // Sitemap generation failed — skip silently\n }\n }\n\n // Generate robots.txt\n if (config.robots) {\n const robots = generateRobots(config.robots)\n\n this.emitFile({\n type: 'asset',\n fileName: 'robots.txt',\n source: robots,\n })\n }\n },\n }\n}\n\n// ─── SEO middleware (serve sitemap/robots in dev) ────────────────────────────\n\n/**\n * SEO middleware for dev server.\n * Serves sitemap.xml and robots.txt dynamically during development.\n */\nexport function seoMiddleware(config: SeoPluginConfig = {}): Middleware {\n return async (ctx) => {\n if (ctx.url.pathname === '/robots.txt' && config.robots) {\n return new Response(generateRobots(config.robots), {\n headers: { 'Content-Type': 'text/plain' },\n })\n }\n\n if (ctx.url.pathname === '/sitemap.xml' && config.sitemap) {\n try {\n const { scanRouteFiles } = await import('./fs-router')\n const routesDir = `${process.cwd()}/src/routes`\n const files = await scanRouteFiles(routesDir)\n const sitemap = generateSitemap(files, config.sitemap)\n\n return new Response(sitemap, {\n headers: { 'Content-Type': 'application/xml' },\n })\n } catch {\n // Sitemap generation failed — continue to rendering\n }\n }\n }\n}\n"],"mappings":";;;;AA2CA,SAAgB,gBACd,YACA,QACQ;CACR,MAAM,EAAE,QAAQ,UAAU,EAAE,EAAE,aAAa,UAAU,WAAW,OAAQ;AA8CxE,QAAO;;EAhB0B,CAC/B,GA7BY,WACX,QAAQ,MAAM;EAEb,MAAM,OAAO,EACV,MAAM,IAAI,CACV,KAAK,EACJ,QAAQ,UAAU,GAAG;AACzB,SAAO,SAAS,aAAa,SAAS,YAAY,SAAS;GAC3D,CACD,KAAK,MAAM;EAEV,IAAI,OAAO,EACR,QAAQ,UAAU,GAAG,CACrB,QAAQ,YAAY,IAAI,CACxB,QAAQ,WAAW,IAAI;AAG1B,MAAI,KAAK,SAAS,IAAI,CAAE,QAAO;AAG/B,SAAO,KAAK,QAAQ,iBAAiB,GAAG;AAExC,MAAI,CAAC,KAAK,WAAW,IAAI,CAAE,QAAO,IAAI;AACtC,SAAO;GACP,CACD,QAAQ,MAAmB,MAAM,KAAK,CACtC,QAAQ,MAAM,CAAC,QAAQ,MAAM,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC,CAG5C,KAAK,OAAO;EAAE,MAAM;EAAG;EAAY;EAAU,EAAE,EACxD,GAAI,OAAO,mBAAmB,EAAE,CACjC,CAGE,KAAK,UAAU;AAEd,SAAO;WACF,UAFO,GAAG,SAAS,MAAM,SAAS,MAAM,KAAK,MAAM,OAErC,CAAC;kBACR,MAAM,cAAc,WAAW;gBACjC,MAAM,YAAY,SAAS,aAAa,MAAM,UAAU,kBAAkB,MAAM,QAAQ,cAAc,GAAG;;GAEnH,CACD,KAAK,KAAK,CAIL;;;AAIV,SAAS,UAAU,KAAqB;AACtC,QAAO,IACJ,QAAQ,MAAM,QAAQ,CACtB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,SAAS,CACvB,QAAQ,MAAM,SAAS;;;;;AAwB5B,SAAgB,eAAe,SAAuB,EAAE,EAAU;CAChE,MAAM,EAAE,QAAQ,CAAC;EAAE,WAAW;EAAK,OAAO,CAAC,IAAI;EAAE,CAAC,EAAE,SAAS,SAAS;CACtE,MAAM,QAAkB,EAAE;AAE1B,MAAK,MAAM,QAAQ,OAAO;AACxB,QAAM,KAAK,eAAe,KAAK,YAAY;AAC3C,MAAI,KAAK,MACP,MAAK,MAAM,QAAQ,KAAK,MAAO,OAAM,KAAK,UAAU,OAAO;AAE7D,MAAI,KAAK,SACP,MAAK,MAAM,QAAQ,KAAK,SAAU,OAAM,KAAK,aAAa,OAAO;AAEnE,MAAI,KAAK,WAAY,OAAM,KAAK,gBAAgB,KAAK,aAAa;AAClE,QAAM,KAAK,GAAG;;AAGhB,KAAI,QAAS,OAAM,KAAK,YAAY,UAAU;AAC9C,KAAI,KAAM,OAAM,KAAK,SAAS,OAAO;AAErC,QAAO,MAAM,KAAK,KAAK;;;;;;;;;;;;;;AA6BzB,SAAgB,OAAO,MAAuC;CAC5D,MAAM,KAAK;EACT,YAAY;EACZ,GAAG;EACJ;AACD,QAAO,sCAAsC,KAAK,UAAU,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;AA8BlE,SAAgB,UAAU,SAA0B,EAAE,EAAU;AAC9D,QAAO;EACL,MAAM;EACN,OAAO;EAEP,MAAM,eAAe,GAAG,SAAS;AAE/B,OAAI,OAAO,SAAS;IAClB,MAAM,EAAE,mBAAmB,MAAM,OAAO;IACxC,MAAM,YAAY,GAAG,QAAQ,KAAK,CAAC;AAEnC,QAAI;KAEF,MAAM,UAAU,gBADF,MAAM,eAAe,UAAU,EACN,OAAO,QAAQ;AAEtD,UAAK,SAAS;MACZ,MAAM;MACN,UAAU;MACV,QAAQ;MACT,CAAC;YACI;;AAMV,OAAI,OAAO,QAAQ;IACjB,MAAM,SAAS,eAAe,OAAO,OAAO;AAE5C,SAAK,SAAS;KACZ,MAAM;KACN,UAAU;KACV,QAAQ;KACT,CAAC;;;EAGP;;;;;;AASH,SAAgB,cAAc,SAA0B,EAAE,EAAc;AACtE,QAAO,OAAO,QAAQ;AACpB,MAAI,IAAI,IAAI,aAAa,iBAAiB,OAAO,OAC/C,QAAO,IAAI,SAAS,eAAe,OAAO,OAAO,EAAE,EACjD,SAAS,EAAE,gBAAgB,cAAc,EAC1C,CAAC;AAGJ,MAAI,IAAI,IAAI,aAAa,kBAAkB,OAAO,QAChD,KAAI;GACF,MAAM,EAAE,mBAAmB,MAAM,OAAO;GAGxC,MAAM,UAAU,gBADF,MAAM,eADF,GAAG,QAAQ,KAAK,CAAC,aACU,EACN,OAAO,QAAQ;AAEtD,UAAO,IAAI,SAAS,SAAS,EAC3B,SAAS,EAAE,gBAAgB,mBAAmB,EAC/C,CAAC;UACI"}
1
+ {"version":3,"file":"seo.js","names":[],"sources":["../src/seo.ts"],"sourcesContent":["import type { Middleware } from \"@pyreon/server\"\nimport type { Plugin } from \"vite\"\n\n// ─── SEO utilities ──────────────────────────────────────────────────────────\n//\n// Zero provides built-in SEO tooling:\n// - Automatic sitemap.xml generation from file-based routes\n// - Configurable robots.txt\n// - Structured data (JSON-LD) helpers\n// - Open Graph / Twitter Card meta helpers\n\nexport interface SitemapConfig {\n /** Base URL of the site (required). e.g. \"https://example.com\" */\n origin: string\n /** Paths to exclude from the sitemap. */\n exclude?: string[]\n /** Default change frequency. Default: \"weekly\" */\n changefreq?: ChangeFreq\n /** Default priority. Default: 0.7 */\n priority?: number\n /** Additional URLs to include (for dynamic routes). */\n additionalPaths?: SitemapEntry[]\n}\n\nexport interface SitemapEntry {\n path: string\n changefreq?: ChangeFreq\n priority?: number\n lastmod?: string\n}\n\nexport type ChangeFreq = \"always\" | \"hourly\" | \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"never\"\n\n/**\n * Generate a sitemap.xml string from route file paths.\n */\nexport function generateSitemap(routeFiles: string[], config: SitemapConfig): string {\n const { origin, exclude = [], changefreq = \"weekly\", priority = 0.7 } = config\n\n const paths = routeFiles\n .filter((f) => {\n // Exclude layout, error, loading files\n const name = f\n .split(\"/\")\n .pop()\n ?.replace(/\\.\\w+$/, \"\")\n return name !== \"_layout\" && name !== \"_error\" && name !== \"_loading\"\n })\n .map((f) => {\n // Convert file path to URL\n let path = f\n .replace(/\\.\\w+$/, \"\")\n .replace(/\\/index$/, \"/\")\n .replace(/^index$/, \"/\")\n\n // Skip dynamic routes — they need additionalPaths\n if (path.includes(\"[\")) return null\n\n // Strip route groups\n path = path.replace(/\\([\\w-]+\\)\\//g, \"\")\n\n if (!path.startsWith(\"/\")) path = `/${path}`\n return path\n })\n .filter((p): p is string => p !== null)\n .filter((p) => !exclude.some((e) => p.startsWith(e)))\n\n const allPaths: SitemapEntry[] = [\n ...paths.map((p) => ({ path: p, changefreq, priority })),\n ...(config.additionalPaths ?? []),\n ]\n\n const entries = allPaths\n .map((entry) => {\n const loc = `${origin}${entry.path === \"/\" ? \"\" : entry.path}`\n return ` <url>\n <loc>${escapeXml(loc)}</loc>\n <changefreq>${entry.changefreq ?? changefreq}</changefreq>\n <priority>${entry.priority ?? priority}</priority>${entry.lastmod ? `\\n <lastmod>${entry.lastmod}</lastmod>` : \"\"}\n </url>`\n })\n .join(\"\\n\")\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${entries}\n</urlset>`\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\")\n}\n\n// ─── Robots.txt ─────────────────────────────────────────────────────────────\n\nexport interface RobotsConfig {\n /** Rules per user-agent. */\n rules?: RobotsRule[]\n /** Sitemap URL. */\n sitemap?: string\n /** Host directive. */\n host?: string\n}\n\nexport interface RobotsRule {\n userAgent: string\n allow?: string[]\n disallow?: string[]\n crawlDelay?: number\n}\n\n/**\n * Generate a robots.txt string.\n */\nexport function generateRobots(config: RobotsConfig = {}): string {\n const { rules = [{ userAgent: \"*\", allow: [\"/\"] }], sitemap, host } = config\n const lines: string[] = []\n\n for (const rule of rules) {\n lines.push(`User-agent: ${rule.userAgent}`)\n if (rule.allow) {\n for (const path of rule.allow) lines.push(`Allow: ${path}`)\n }\n if (rule.disallow) {\n for (const path of rule.disallow) lines.push(`Disallow: ${path}`)\n }\n if (rule.crawlDelay) lines.push(`Crawl-delay: ${rule.crawlDelay}`)\n lines.push(\"\")\n }\n\n if (sitemap) lines.push(`Sitemap: ${sitemap}`)\n if (host) lines.push(`Host: ${host}`)\n\n return lines.join(\"\\n\")\n}\n\n// ─── Structured data (JSON-LD) ──────────────────────────────────────────────\n\nexport type JsonLdType =\n | \"WebSite\"\n | \"WebPage\"\n | \"Article\"\n | \"BlogPosting\"\n | \"Product\"\n | \"Organization\"\n | \"Person\"\n | \"BreadcrumbList\"\n | \"FAQPage\"\n | (string & {})\n\n/**\n * Generate a JSON-LD script tag string for structured data.\n *\n * @example\n * useHead({\n * script: [jsonLd({\n * \"@type\": \"WebSite\",\n * name: \"My Site\",\n * url: \"https://example.com\",\n * })],\n * })\n */\nexport function jsonLd(data: Record<string, unknown>): string {\n const ld = {\n \"@context\": \"https://schema.org\",\n ...data,\n }\n return `<script type=\"application/ld+json\">${JSON.stringify(ld)}</script>`\n}\n\n// ─── SEO Vite plugin ────────────────────────────────────────────────────────\n\nexport interface SeoPluginConfig {\n /** Sitemap configuration. */\n sitemap?: SitemapConfig\n /** Robots.txt configuration. */\n robots?: RobotsConfig\n}\n\n/**\n * Zero SEO Vite plugin.\n * Generates sitemap.xml and robots.txt at build time.\n *\n * @example\n * import { seoPlugin } from \"@pyreon/zero/seo\"\n *\n * export default {\n * plugins: [\n * pyreon(),\n * zero(),\n * seoPlugin({\n * sitemap: { origin: \"https://example.com\" },\n * robots: { sitemap: \"https://example.com/sitemap.xml\" },\n * }),\n * ],\n * }\n */\nexport function seoPlugin(config: SeoPluginConfig = {}): Plugin {\n return {\n name: \"pyreon-zero-seo\",\n apply: \"build\",\n\n async generateBundle(_, _bundle) {\n // Generate sitemap.xml\n if (config.sitemap) {\n const { scanRouteFiles } = await import(\"./fs-router\")\n const routesDir = `${process.cwd()}/src/routes`\n\n try {\n const files = await scanRouteFiles(routesDir)\n const sitemap = generateSitemap(files, config.sitemap)\n\n this.emitFile({\n type: \"asset\",\n fileName: \"sitemap.xml\",\n source: sitemap,\n })\n } catch {\n // Sitemap generation failed — skip silently\n }\n }\n\n // Generate robots.txt\n if (config.robots) {\n const robots = generateRobots(config.robots)\n\n this.emitFile({\n type: \"asset\",\n fileName: \"robots.txt\",\n source: robots,\n })\n }\n },\n }\n}\n\n// ─── SEO middleware (serve sitemap/robots in dev) ────────────────────────────\n\n/**\n * SEO middleware for dev server.\n * Serves sitemap.xml and robots.txt dynamically during development.\n */\nexport function seoMiddleware(config: SeoPluginConfig = {}): Middleware {\n return async (ctx) => {\n if (ctx.url.pathname === \"/robots.txt\" && config.robots) {\n return new Response(generateRobots(config.robots), {\n headers: { \"Content-Type\": \"text/plain\" },\n })\n }\n\n if (ctx.url.pathname === \"/sitemap.xml\" && config.sitemap) {\n try {\n const { scanRouteFiles } = await import(\"./fs-router\")\n const routesDir = `${process.cwd()}/src/routes`\n const files = await scanRouteFiles(routesDir)\n const sitemap = generateSitemap(files, config.sitemap)\n\n return new Response(sitemap, {\n headers: { \"Content-Type\": \"application/xml\" },\n })\n } catch {\n // Sitemap generation failed — continue to rendering\n }\n }\n }\n}\n"],"mappings":"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"}
package/lib/theme.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { onMount, onUnmount } from "@pyreon/core";
2
2
  import { effect, signal } from "@pyreon/reactivity";
3
3
 
4
- //#region ../../node_modules/.bun/@pyreon+core@0.7.5/node_modules/@pyreon/core/lib/jsx-runtime.js
4
+ //#region ../../core/core/lib/jsx-runtime.js
5
5
  /**
6
6
  * Hyperscript function — the compiled output of JSX.
7
7
  * `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
@@ -113,7 +113,7 @@ function ThemeToggle(props) {
113
113
  return /* @__PURE__ */ jsx("button", {
114
114
  class: props.class,
115
115
  style: props.style,
116
- onclick: toggleTheme,
116
+ onClick: toggleTheme,
117
117
  "aria-label": "Toggle theme",
118
118
  title: "Toggle theme",
119
119
  type: "button",
package/lib/theme.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"theme.js","names":[],"sources":["../../../node_modules/.bun/@pyreon+core@0.7.5/node_modules/@pyreon/core/lib/jsx-runtime.js","../src/theme.tsx"],"sourcesContent":["//#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 { 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/** 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 'dark'\n return window.matchMedia('(prefers-color-scheme: dark)').matches\n ? 'dark'\n : '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\n const dispose = effect(() => {\n document.documentElement.dataset.theme = resolvedTheme()\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}catch(e){}})()`\n"],"x_google_ignoreList":[0],"mappings":"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"}
1
+ {"version":3,"file":"theme.js","names":[],"sources":["../../../core/core/lib/jsx-runtime.js","../src/theme.tsx"],"sourcesContent":["//#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 { 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/** 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 \"dark\"\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\n const dispose = effect(() => {\n document.documentElement.dataset.theme = resolvedTheme()\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}catch(e){}})()`\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@pyreon/zero",
3
- "version": "0.4.1",
3
+ "version": "0.11.0",
4
4
  "description": "Pyreon Zero — zero-config full-stack framework powered by Pyreon and Vite",
5
5
  "license": "MIT",
6
6
  "author": "Vit Bokisch",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/pyreon/zero",
10
- "directory": "packages/zero"
9
+ "url": "https://github.com/pyreon/pyreon.git",
10
+ "directory": "packages/zero/zero"
11
11
  },
12
12
  "type": "module",
13
13
  "files": [
@@ -112,20 +112,21 @@
112
112
  "build": "vl_rolldown_build && tsc",
113
113
  "dev": "vl_rolldown_build-watch",
114
114
  "test": "vitest run",
115
- "typecheck": "tsc --noEmit"
115
+ "typecheck": "tsc --noEmit",
116
+ "lint": "biome check ."
116
117
  },
117
118
  "dependencies": {
118
- "@pyreon/core": "^0.7.5",
119
- "@pyreon/head": "^0.7.5",
120
- "@pyreon/meta": "^0.4.1",
121
- "@pyreon/router": "^0.7.5",
122
- "@pyreon/runtime-dom": "^0.7.5",
123
- "@pyreon/runtime-server": "^0.7.5",
124
- "@pyreon/server": "^0.7.5",
125
- "@pyreon/vite-plugin": "^0.7.5",
119
+ "@pyreon/core": "^0.11.0",
120
+ "@pyreon/head": "^0.11.0",
121
+ "@pyreon/meta": "^0.11.0",
122
+ "@pyreon/router": "^0.11.0",
123
+ "@pyreon/runtime-dom": "^0.11.0",
124
+ "@pyreon/runtime-server": "^0.11.0",
125
+ "@pyreon/server": "^0.11.0",
126
+ "@pyreon/vite-plugin": "^0.11.0",
126
127
  "vite": "^8.0.0"
127
128
  },
128
129
  "peerDependencies": {
129
- "@pyreon/reactivity": "^0.7.5"
130
+ "@pyreon/reactivity": "^0.11.0"
130
131
  }
131
132
  }
package/src/actions.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MiddlewareContext } from '@pyreon/server'
1
+ import type { MiddlewareContext } from "@pyreon/server"
2
2
 
3
3
  // ─── Types ───────────────────────────────────────────────────────────────────
4
4
 
@@ -52,40 +52,35 @@ let actionCounter = 0
52
52
  * // In a component:
53
53
  * const result = await createPost({ title: 'Hello', body: '...' })
54
54
  */
55
- export function defineAction<T = unknown>(
56
- handler: ActionHandler<T>,
57
- ): Action<T> {
55
+ export function defineAction<T = unknown>(handler: ActionHandler<T>): Action<T> {
58
56
  const id = `action_${actionCounter++}`
59
57
 
60
58
  actionRegistry.set(id, { id, handler: handler as ActionHandler })
61
59
 
62
60
  const callable = async (data?: unknown): Promise<T> => {
63
61
  // Server-side: execute handler directly (no network round-trip)
64
- if (typeof globalThis.window === 'undefined') {
62
+ if (typeof globalThis.window === "undefined") {
65
63
  return handler({
66
64
  request: new Request(`http://localhost/_zero/actions/${id}`, {
67
- method: 'POST',
68
- headers: { 'Content-Type': 'application/json' },
65
+ method: "POST",
66
+ headers: { "Content-Type": "application/json" },
69
67
  body: JSON.stringify(data ?? null),
70
68
  }),
71
69
  formData: null,
72
70
  json: data ?? null,
73
- headers: new Headers({ 'Content-Type': 'application/json' }),
71
+ headers: new Headers({ "Content-Type": "application/json" }),
74
72
  })
75
73
  }
76
74
 
77
75
  // Client-side: POST to the action endpoint
78
76
  const response = await fetch(`/_zero/actions/${id}`, {
79
- method: 'POST',
80
- headers: { 'Content-Type': 'application/json' },
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json" },
81
79
  body: JSON.stringify(data ?? null),
82
80
  })
83
81
  if (!response.ok) {
84
82
  const body = await response.json().catch(() => ({}))
85
- throw new Error(
86
- (body as { error?: string }).error ??
87
- `Action failed: ${response.statusText}`,
88
- )
83
+ throw new Error((body as { error?: string }).error ?? `Action failed: ${response.statusText}`)
89
84
  }
90
85
  return response.json()
91
86
  }
@@ -118,37 +113,34 @@ export function createActionMiddleware(): (
118
113
  ctx: MiddlewareContext,
119
114
  ) => Response | undefined | Promise<Response | undefined> {
120
115
  return async (ctx: MiddlewareContext) => {
121
- if (!ctx.path.startsWith('/_zero/actions/')) return
116
+ if (!ctx.path.startsWith("/_zero/actions/")) return
122
117
 
123
- const actionId = ctx.path.slice('/_zero/actions/'.length)
118
+ const actionId = ctx.path.slice("/_zero/actions/".length)
124
119
  const action = actionRegistry.get(actionId)
125
120
 
126
121
  if (!action) {
127
- return Response.json({ error: 'Action not found' }, { status: 404 })
122
+ return Response.json({ error: "Action not found" }, { status: 404 })
128
123
  }
129
124
 
130
- if (ctx.req.method !== 'POST') {
131
- return Response.json({ error: 'Method not allowed' }, { status: 405 })
125
+ if (ctx.req.method !== "POST") {
126
+ return Response.json({ error: "Method not allowed" }, { status: 405 })
132
127
  }
133
128
 
134
129
  return executeAction(action, ctx.req)
135
130
  }
136
131
  }
137
132
 
138
- async function executeAction(
139
- action: RegisteredAction,
140
- req: Request,
141
- ): Promise<Response> {
133
+ async function executeAction(action: RegisteredAction, req: Request): Promise<Response> {
142
134
  try {
143
- const contentType = req.headers.get('content-type') ?? ''
135
+ const contentType = req.headers.get("content-type") ?? ""
144
136
  let formData: FormData | null = null
145
137
  let json: unknown = null
146
138
 
147
- if (contentType.includes('application/json')) {
139
+ if (contentType.includes("application/json")) {
148
140
  json = await req.json()
149
141
  } else if (
150
- contentType.includes('multipart/form-data') ||
151
- contentType.includes('application/x-www-form-urlencoded')
142
+ contentType.includes("multipart/form-data") ||
143
+ contentType.includes("application/x-www-form-urlencoded")
152
144
  ) {
153
145
  formData = await req.formData()
154
146
  }
@@ -162,7 +154,7 @@ async function executeAction(
162
154
 
163
155
  return Response.json(result ?? null)
164
156
  } catch (err) {
165
- const message = err instanceof Error ? err.message : 'Internal server error'
157
+ const message = err instanceof Error ? err.message : "Internal server error"
166
158
  return Response.json({ error: message }, { status: 500 })
167
159
  }
168
160
  }
@@ -1,23 +1,23 @@
1
- import type { Adapter, AdapterBuildOptions } from '../types'
1
+ import type { Adapter, AdapterBuildOptions } from "../types"
2
2
 
3
3
  /**
4
4
  * Bun adapter — generates a standalone Bun.serve() entry.
5
5
  */
6
6
  export function bunAdapter(): Adapter {
7
7
  return {
8
- name: 'bun',
8
+ name: "bun",
9
9
  async build(options: AdapterBuildOptions) {
10
- const { writeFile, cp, mkdir } = await import('node:fs/promises')
11
- const { join } = await import('node:path')
10
+ const { writeFile, cp, mkdir } = await import("node:fs/promises")
11
+ const { join } = await import("node:path")
12
12
 
13
13
  const outDir = options.outDir
14
14
  await mkdir(outDir, { recursive: true })
15
15
 
16
16
  // Copy server and client builds
17
- await cp(options.clientOutDir, join(outDir, 'client'), {
17
+ await cp(options.clientOutDir, join(outDir, "client"), {
18
18
  recursive: true,
19
19
  })
20
- await cp(join(options.serverEntry, '..'), join(outDir, 'server'), {
20
+ await cp(join(options.serverEntry, ".."), join(outDir, "server"), {
21
21
  recursive: true,
22
22
  })
23
23
 
@@ -59,7 +59,7 @@ Bun.serve({
59
59
  console.log("\\n ⚡ Zero production server running on http://localhost:${port}\\n")
60
60
  `.trimStart()
61
61
 
62
- await writeFile(join(outDir, 'index.ts'), serverEntry)
62
+ await writeFile(join(outDir, "index.ts"), serverEntry)
63
63
  },
64
64
  }
65
65
  }
@@ -1,29 +1,27 @@
1
- export { bunAdapter } from './bun'
2
- export { nodeAdapter } from './node'
3
- export { staticAdapter } from './static'
1
+ export { bunAdapter } from "./bun"
2
+ export { nodeAdapter } from "./node"
3
+ export { staticAdapter } from "./static"
4
4
 
5
- import type { Adapter, ZeroConfig } from '../types'
6
- import { bunAdapter } from './bun'
7
- import { nodeAdapter } from './node'
8
- import { staticAdapter } from './static'
5
+ import type { Adapter, ZeroConfig } from "../types"
6
+ import { bunAdapter } from "./bun"
7
+ import { nodeAdapter } from "./node"
8
+ import { staticAdapter } from "./static"
9
9
 
10
10
  /**
11
11
  * Resolve the adapter from config.
12
12
  * Returns a built-in adapter or throws if unknown.
13
13
  */
14
14
  export function resolveAdapter(config: ZeroConfig): Adapter {
15
- const name = config.adapter ?? 'node'
15
+ const name = config.adapter ?? "node"
16
16
 
17
17
  switch (name) {
18
- case 'node':
18
+ case "node":
19
19
  return nodeAdapter()
20
- case 'bun':
20
+ case "bun":
21
21
  return bunAdapter()
22
- case 'static':
22
+ case "static":
23
23
  return staticAdapter()
24
24
  default:
25
- throw new Error(
26
- `[zero] Unknown adapter: "${name}". Use "node", "bun", or "static".`,
27
- )
25
+ throw new Error(`[zero] Unknown adapter: "${name}". Use "node", "bun", or "static".`)
28
26
  }
29
27
  }
@@ -1,23 +1,23 @@
1
- import type { Adapter, AdapterBuildOptions } from '../types'
1
+ import type { Adapter, AdapterBuildOptions } from "../types"
2
2
 
3
3
  /**
4
4
  * Node.js adapter — generates a standalone server entry using node:http.
5
5
  */
6
6
  export function nodeAdapter(): Adapter {
7
7
  return {
8
- name: 'node',
8
+ name: "node",
9
9
  async build(options: AdapterBuildOptions) {
10
- const { writeFile, cp, mkdir } = await import('node:fs/promises')
11
- const { join } = await import('node:path')
10
+ const { writeFile, cp, mkdir } = await import("node:fs/promises")
11
+ const { join } = await import("node:path")
12
12
 
13
13
  const outDir = options.outDir
14
14
  await mkdir(outDir, { recursive: true })
15
15
 
16
16
  // Copy server and client builds
17
- await cp(options.clientOutDir, join(outDir, 'client'), {
17
+ await cp(options.clientOutDir, join(outDir, "client"), {
18
18
  recursive: true,
19
19
  })
20
- await cp(join(options.serverEntry, '..'), join(outDir, 'server'), {
20
+ await cp(join(options.serverEntry, ".."), join(outDir, "server"), {
21
21
  recursive: true,
22
22
  })
23
23
 
@@ -103,11 +103,8 @@ server.listen(${port}, () => {
103
103
  })
104
104
  `.trimStart()
105
105
 
106
- await writeFile(join(outDir, 'index.js'), serverEntry)
107
- await writeFile(
108
- join(outDir, 'package.json'),
109
- JSON.stringify({ type: 'module' }, null, 2),
110
- )
106
+ await writeFile(join(outDir, "index.js"), serverEntry)
107
+ await writeFile(join(outDir, "package.json"), JSON.stringify({ type: "module" }, null, 2))
111
108
  },
112
109
  }
113
110
  }