@pyreon/zero 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/lib/fs-router-BkbIWqek.js.map +1 -1
- package/lib/{fs-router-jfd1QGLB.js → fs-router-n4VA4lxu.js} +29 -4
- package/lib/fs-router-n4VA4lxu.js.map +1 -0
- package/lib/image.js +50 -1
- package/lib/image.js.map +1 -1
- package/lib/index.js +651 -11
- package/lib/index.js.map +1 -1
- package/lib/link.js +49 -1
- package/lib/link.js.map +1 -1
- package/lib/script.js +49 -1
- package/lib/script.js.map +1 -1
- package/lib/theme.js +50 -1
- package/lib/theme.js.map +1 -1
- package/lib/types/actions.d.ts +57 -0
- package/lib/types/actions.d.ts.map +1 -0
- package/lib/types/api-routes.d.ts +66 -0
- package/lib/types/api-routes.d.ts.map +1 -0
- package/lib/types/compression.d.ts +33 -0
- package/lib/types/compression.d.ts.map +1 -0
- package/lib/types/cors.d.ts +32 -0
- package/lib/types/cors.d.ts.map +1 -0
- package/lib/types/entry-server.d.ts +10 -2
- package/lib/types/entry-server.d.ts.map +1 -1
- package/lib/types/error-overlay.d.ts +6 -0
- package/lib/types/error-overlay.d.ts.map +1 -0
- package/lib/types/fs-router.d.ts +5 -0
- package/lib/types/fs-router.d.ts.map +1 -1
- package/lib/types/image.d.ts +1 -1
- package/lib/types/image.d.ts.map +1 -1
- package/lib/types/index.d.ts +12 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/rate-limit.d.ts +34 -0
- package/lib/types/rate-limit.d.ts.map +1 -0
- package/lib/types/script.d.ts +1 -1
- package/lib/types/script.d.ts.map +1 -1
- package/lib/types/testing.d.ts +85 -0
- package/lib/types/testing.d.ts.map +1 -0
- package/lib/types/theme.d.ts +1 -1
- package/lib/types/theme.d.ts.map +1 -1
- package/lib/types/types.d.ts +5 -0
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/vite-plugin.d.ts.map +1 -1
- package/package.json +40 -9
- package/src/actions.ts +168 -0
- package/src/api-routes.ts +233 -0
- package/src/compression.ts +107 -0
- package/src/cors.ts +102 -0
- package/src/entry-server.ts +62 -7
- package/src/error-overlay.ts +121 -0
- package/src/fs-router.ts +34 -2
- package/src/index.ts +37 -0
- package/src/rate-limit.ts +122 -0
- package/src/testing.ts +150 -0
- package/src/types.ts +8 -0
- package/src/vite-plugin.ts +75 -10
- package/lib/fs-router-jfd1QGLB.js.map +0 -1
package/lib/link.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createRef, onMount, onUnmount } from "@pyreon/core";
|
|
2
2
|
import { useRouter } from "@pyreon/router";
|
|
3
|
-
import { jsx } from "@pyreon/core/jsx-runtime";
|
|
4
3
|
|
|
5
4
|
//#region src/utils/use-intersection-observer.ts
|
|
6
5
|
/**
|
|
@@ -26,6 +25,55 @@ function useIntersectionObserver(getElement, onIntersect, rootMargin = "200px")
|
|
|
26
25
|
});
|
|
27
26
|
}
|
|
28
27
|
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region ../../node_modules/.bun/@pyreon+core@0.5.5/node_modules/@pyreon/core/lib/jsx-runtime.js
|
|
30
|
+
/**
|
|
31
|
+
* Hyperscript function — the compiled output of JSX.
|
|
32
|
+
* `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
|
|
33
|
+
*
|
|
34
|
+
* Generic on P so TypeScript validates props match the component's signature
|
|
35
|
+
* at the call site, then stores the result in the loosely-typed VNode.
|
|
36
|
+
*/
|
|
37
|
+
/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */
|
|
38
|
+
const EMPTY_PROPS = {};
|
|
39
|
+
function h(type, props, ...children) {
|
|
40
|
+
return {
|
|
41
|
+
type,
|
|
42
|
+
props: props ?? EMPTY_PROPS,
|
|
43
|
+
children: normalizeChildren(children),
|
|
44
|
+
key: props?.key ?? null
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function normalizeChildren(children) {
|
|
48
|
+
for (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);
|
|
49
|
+
return children;
|
|
50
|
+
}
|
|
51
|
+
function flattenChildren(children) {
|
|
52
|
+
const result = [];
|
|
53
|
+
for (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));
|
|
54
|
+
else result.push(child);
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* JSX automatic runtime.
|
|
59
|
+
*
|
|
60
|
+
* When tsconfig has `"jsxImportSource": "@pyreon/core"`, the TS/bundler compiler
|
|
61
|
+
* rewrites JSX to imports from this file automatically:
|
|
62
|
+
* <div class="x" /> → jsx("div", { class: "x" })
|
|
63
|
+
*/
|
|
64
|
+
function jsx(type, props, key) {
|
|
65
|
+
const { children, ...rest } = props;
|
|
66
|
+
const propsWithKey = key != null ? {
|
|
67
|
+
...rest,
|
|
68
|
+
key
|
|
69
|
+
} : rest;
|
|
70
|
+
if (typeof type === "function") return h(type, children !== void 0 ? {
|
|
71
|
+
...propsWithKey,
|
|
72
|
+
children
|
|
73
|
+
} : propsWithKey);
|
|
74
|
+
return h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);
|
|
75
|
+
}
|
|
76
|
+
|
|
29
77
|
//#endregion
|
|
30
78
|
//#region src/link.tsx
|
|
31
79
|
const prefetched = /* @__PURE__ */ new Set();
|
package/lib/link.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../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","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 style={props.style}\n target={props.external ? '_blank' : undefined}\n rel={props.external ? 'noopener noreferrer' : undefined}\n 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 class={props.class}\n style={props.style}\n target={props.target}\n rel={props.rel}\n aria-label={props['aria-label']}\n aria-current={props.isExactActive() ? 'page' : undefined}\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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"}
|
|
1
|
+
{"version":3,"file":"link.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../../../node_modules/.bun/@pyreon+core@0.5.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 style={props.style}\n target={props.external ? '_blank' : undefined}\n rel={props.external ? 'noopener noreferrer' : undefined}\n 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 class={props.class}\n style={props.style}\n target={props.target}\n rel={props.rel}\n aria-label={props['aria-label']}\n aria-current={props.isExactActive() ? 'page' : undefined}\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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"}
|
package/lib/script.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createRef, onMount, onUnmount } from "@pyreon/core";
|
|
2
|
-
import { jsx } from "@pyreon/core/jsx-runtime";
|
|
3
2
|
|
|
4
3
|
//#region src/utils/use-intersection-observer.ts
|
|
5
4
|
/**
|
|
@@ -25,6 +24,55 @@ function useIntersectionObserver(getElement, onIntersect, rootMargin = "200px")
|
|
|
25
24
|
});
|
|
26
25
|
}
|
|
27
26
|
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region ../../node_modules/.bun/@pyreon+core@0.5.5/node_modules/@pyreon/core/lib/jsx-runtime.js
|
|
29
|
+
/**
|
|
30
|
+
* Hyperscript function — the compiled output of JSX.
|
|
31
|
+
* `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
|
|
32
|
+
*
|
|
33
|
+
* Generic on P so TypeScript validates props match the component's signature
|
|
34
|
+
* at the call site, then stores the result in the loosely-typed VNode.
|
|
35
|
+
*/
|
|
36
|
+
/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */
|
|
37
|
+
const EMPTY_PROPS = {};
|
|
38
|
+
function h(type, props, ...children) {
|
|
39
|
+
return {
|
|
40
|
+
type,
|
|
41
|
+
props: props ?? EMPTY_PROPS,
|
|
42
|
+
children: normalizeChildren(children),
|
|
43
|
+
key: props?.key ?? null
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function normalizeChildren(children) {
|
|
47
|
+
for (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);
|
|
48
|
+
return children;
|
|
49
|
+
}
|
|
50
|
+
function flattenChildren(children) {
|
|
51
|
+
const result = [];
|
|
52
|
+
for (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));
|
|
53
|
+
else result.push(child);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* JSX automatic runtime.
|
|
58
|
+
*
|
|
59
|
+
* When tsconfig has `"jsxImportSource": "@pyreon/core"`, the TS/bundler compiler
|
|
60
|
+
* rewrites JSX to imports from this file automatically:
|
|
61
|
+
* <div class="x" /> → jsx("div", { class: "x" })
|
|
62
|
+
*/
|
|
63
|
+
function jsx(type, props, key) {
|
|
64
|
+
const { children, ...rest } = props;
|
|
65
|
+
const propsWithKey = key != null ? {
|
|
66
|
+
...rest,
|
|
67
|
+
key
|
|
68
|
+
} : rest;
|
|
69
|
+
if (typeof type === "function") return h(type, children !== void 0 ? {
|
|
70
|
+
...propsWithKey,
|
|
71
|
+
children
|
|
72
|
+
} : propsWithKey);
|
|
73
|
+
return h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);
|
|
74
|
+
}
|
|
75
|
+
|
|
28
76
|
//#endregion
|
|
29
77
|
//#region src/script.tsx
|
|
30
78
|
/**
|
package/lib/script.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"script.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../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","import { 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) {\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"],"mappings":"
|
|
1
|
+
{"version":3,"file":"script.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../../../node_modules/.bun/@pyreon+core@0.5.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 { 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) {\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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"}
|
package/lib/theme.js
CHANGED
|
@@ -1,7 +1,56 @@
|
|
|
1
1
|
import { onMount, onUnmount } from "@pyreon/core";
|
|
2
2
|
import { effect, signal } from "@pyreon/reactivity";
|
|
3
|
-
import { jsx, jsxs } from "@pyreon/core/jsx-runtime";
|
|
4
3
|
|
|
4
|
+
//#region ../../node_modules/.bun/@pyreon+core@0.5.5/node_modules/@pyreon/core/lib/jsx-runtime.js
|
|
5
|
+
/**
|
|
6
|
+
* Hyperscript function — the compiled output of JSX.
|
|
7
|
+
* `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
|
|
8
|
+
*
|
|
9
|
+
* Generic on P so TypeScript validates props match the component's signature
|
|
10
|
+
* at the call site, then stores the result in the loosely-typed VNode.
|
|
11
|
+
*/
|
|
12
|
+
/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */
|
|
13
|
+
const EMPTY_PROPS = {};
|
|
14
|
+
function h(type, props, ...children) {
|
|
15
|
+
return {
|
|
16
|
+
type,
|
|
17
|
+
props: props ?? EMPTY_PROPS,
|
|
18
|
+
children: normalizeChildren(children),
|
|
19
|
+
key: props?.key ?? null
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function normalizeChildren(children) {
|
|
23
|
+
for (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);
|
|
24
|
+
return children;
|
|
25
|
+
}
|
|
26
|
+
function flattenChildren(children) {
|
|
27
|
+
const result = [];
|
|
28
|
+
for (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));
|
|
29
|
+
else result.push(child);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* JSX automatic runtime.
|
|
34
|
+
*
|
|
35
|
+
* When tsconfig has `"jsxImportSource": "@pyreon/core"`, the TS/bundler compiler
|
|
36
|
+
* rewrites JSX to imports from this file automatically:
|
|
37
|
+
* <div class="x" /> → jsx("div", { class: "x" })
|
|
38
|
+
*/
|
|
39
|
+
function jsx(type, props, key) {
|
|
40
|
+
const { children, ...rest } = props;
|
|
41
|
+
const propsWithKey = key != null ? {
|
|
42
|
+
...rest,
|
|
43
|
+
key
|
|
44
|
+
} : rest;
|
|
45
|
+
if (typeof type === "function") return h(type, children !== void 0 ? {
|
|
46
|
+
...propsWithKey,
|
|
47
|
+
children
|
|
48
|
+
} : propsWithKey);
|
|
49
|
+
return h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);
|
|
50
|
+
}
|
|
51
|
+
const jsxs = jsx;
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
5
54
|
//#region src/theme.tsx
|
|
6
55
|
const STORAGE_KEY = "zero-theme";
|
|
7
56
|
/** Reactive theme signal. */
|
package/lib/theme.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.js","names":[],"sources":["../src/theme.tsx"],"sourcesContent":["import { 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 }) {\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":"
|
|
1
|
+
{"version":3,"file":"theme.js","names":[],"sources":["../../../node_modules/.bun/@pyreon+core@0.5.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 { 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 }) {\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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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { MiddlewareContext } from '@pyreon/server';
|
|
2
|
+
/** Context passed to server action handlers. */
|
|
3
|
+
export interface ActionContext {
|
|
4
|
+
/** The original request. */
|
|
5
|
+
request: Request;
|
|
6
|
+
/** Parsed form data (for form submissions). */
|
|
7
|
+
formData: FormData | null;
|
|
8
|
+
/** Parsed JSON body (for JSON submissions). */
|
|
9
|
+
json: unknown;
|
|
10
|
+
/** Request headers. */
|
|
11
|
+
headers: Headers;
|
|
12
|
+
}
|
|
13
|
+
/** A server action handler function. */
|
|
14
|
+
export type ActionHandler<T = unknown> = (ctx: ActionContext) => T | Promise<T>;
|
|
15
|
+
/** A registered action with its ID and handler. */
|
|
16
|
+
interface RegisteredAction {
|
|
17
|
+
id: string;
|
|
18
|
+
handler: ActionHandler;
|
|
19
|
+
}
|
|
20
|
+
/** Client-side callable action returned by defineAction. */
|
|
21
|
+
export interface Action<T = unknown> {
|
|
22
|
+
/** Call the action with JSON data. */
|
|
23
|
+
(data?: unknown): Promise<T>;
|
|
24
|
+
/** The action's unique ID. */
|
|
25
|
+
actionId: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Define a server action. Returns a callable function that:
|
|
29
|
+
* - On the **client**: sends a POST request to `/_zero/actions/<id>`
|
|
30
|
+
* - On the **server** (SSR): executes the handler directly (no fetch)
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // In a route file or module:
|
|
34
|
+
* export const createPost = defineAction(async (ctx) => {
|
|
35
|
+
* const data = ctx.json as { title: string; body: string }
|
|
36
|
+
* // ... save to database
|
|
37
|
+
* return { success: true, id: 123 }
|
|
38
|
+
* })
|
|
39
|
+
*
|
|
40
|
+
* // In a component:
|
|
41
|
+
* const result = await createPost({ title: 'Hello', body: '...' })
|
|
42
|
+
*/
|
|
43
|
+
export declare function defineAction<T = unknown>(handler: ActionHandler<T>): Action<T>;
|
|
44
|
+
/** Get all registered actions. Useful for testing. */
|
|
45
|
+
export declare function getRegisteredActions(): Map<string, RegisteredAction>;
|
|
46
|
+
/**
|
|
47
|
+
* Reset the action registry. Useful for testing.
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export declare function _resetActions(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Create a middleware that handles action requests at `/_zero/actions/*`.
|
|
53
|
+
* Mount this before the SSR handler in the server entry.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createActionMiddleware(): (ctx: MiddlewareContext) => Response | undefined | Promise<Response | undefined>;
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAIvD,gDAAgD;AAChD,MAAM,WAAW,aAAa;IAC5B,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAA;IAChB,+CAA+C;IAC/C,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAA;IACzB,+CAA+C;IAC/C,IAAI,EAAE,OAAO,CAAA;IACb,uBAAuB;IACvB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,wCAAwC;AACxC,MAAM,MAAM,aAAa,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,GAAG,EAAE,aAAa,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;AAE/E,mDAAmD;AACnD,UAAU,gBAAgB;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,aAAa,CAAA;CACvB;AAED,4DAA4D;AAC5D,MAAM,WAAW,MAAM,CAAC,CAAC,GAAG,OAAO;IACjC,sCAAsC;IACtC,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IAC5B,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,CAAA;CACjB;AAOD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,CAAC,GAAG,OAAO,EACtC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,GACxB,MAAM,CAAC,CAAC,CAAC,CAsCX;AAED,sDAAsD;AACtD,wBAAgB,oBAAoB,IAAI,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAEpE;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAGpC;AAID;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,CACxC,GAAG,EAAE,iBAAiB,KACnB,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAiBxD"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Middleware } from '@pyreon/server';
|
|
2
|
+
/** HTTP methods supported by API routes. */
|
|
3
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
4
|
+
/** Context passed to API route handlers. */
|
|
5
|
+
export interface ApiContext {
|
|
6
|
+
/** The incoming request. */
|
|
7
|
+
request: Request;
|
|
8
|
+
/** Parsed URL. */
|
|
9
|
+
url: URL;
|
|
10
|
+
/** URL path. */
|
|
11
|
+
path: string;
|
|
12
|
+
/** Dynamic route parameters (e.g., { id: "123" }). */
|
|
13
|
+
params: Record<string, string>;
|
|
14
|
+
/** Request headers. */
|
|
15
|
+
headers: Headers;
|
|
16
|
+
}
|
|
17
|
+
/** An API route handler function. */
|
|
18
|
+
export type ApiHandler = (ctx: ApiContext) => Response | Promise<Response>;
|
|
19
|
+
/** An API route module — exports named HTTP method handlers. */
|
|
20
|
+
export interface ApiRouteModule {
|
|
21
|
+
GET?: ApiHandler;
|
|
22
|
+
POST?: ApiHandler;
|
|
23
|
+
PUT?: ApiHandler;
|
|
24
|
+
PATCH?: ApiHandler;
|
|
25
|
+
DELETE?: ApiHandler;
|
|
26
|
+
HEAD?: ApiHandler;
|
|
27
|
+
OPTIONS?: ApiHandler;
|
|
28
|
+
}
|
|
29
|
+
/** A registered API route entry. */
|
|
30
|
+
export interface ApiRouteEntry {
|
|
31
|
+
/** URL pattern (e.g., "/api/posts/:id"). */
|
|
32
|
+
pattern: string;
|
|
33
|
+
/** The route module with method handlers. */
|
|
34
|
+
module: ApiRouteModule;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Match a URL path against an API route pattern.
|
|
38
|
+
* Returns extracted params or null if no match.
|
|
39
|
+
*/
|
|
40
|
+
export declare function matchApiRoute(pattern: string, path: string): Record<string, string> | null;
|
|
41
|
+
/**
|
|
42
|
+
* Create a middleware that dispatches API route requests.
|
|
43
|
+
* API routes are matched by URL pattern and HTTP method.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createApiMiddleware(routes: ApiRouteEntry[]): Middleware;
|
|
46
|
+
/**
|
|
47
|
+
* Detect whether a route file is an API route.
|
|
48
|
+
* API routes are `.ts` or `.js` files inside an `api/` directory.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isApiRoute(filePath: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Convert an API route file path to a URL pattern.
|
|
53
|
+
*
|
|
54
|
+
* Examples:
|
|
55
|
+
* "api/posts.ts" → "/api/posts"
|
|
56
|
+
* "api/posts/index.ts" → "/api/posts"
|
|
57
|
+
* "api/posts/[id].ts" → "/api/posts/:id"
|
|
58
|
+
* "api/[...path].ts" → "/api/:path*"
|
|
59
|
+
*/
|
|
60
|
+
export declare function apiFilePathToPattern(filePath: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* Generate a virtual module that exports API route entries.
|
|
63
|
+
* Each entry maps a URL pattern to a module with HTTP method handlers.
|
|
64
|
+
*/
|
|
65
|
+
export declare function generateApiRouteModule(files: string[], routesDir: string): string;
|
|
66
|
+
//# sourceMappingURL=api-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-routes.d.ts","sourceRoot":"","sources":["../../src/api-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,gBAAgB,CAAA;AAInE,4CAA4C;AAC5C,MAAM,MAAM,UAAU,GAClB,KAAK,GACL,MAAM,GACN,KAAK,GACL,OAAO,GACP,QAAQ,GACR,MAAM,GACN,SAAS,CAAA;AAEb,4CAA4C;AAC5C,MAAM,WAAW,UAAU;IACzB,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAA;IAChB,kBAAkB;IAClB,GAAG,EAAE,GAAG,CAAA;IACR,gBAAgB;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,uBAAuB;IACvB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,qCAAqC;AACrC,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,UAAU,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAE1E,gEAAgE;AAChE,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,UAAU,CAAA;IAChB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,GAAG,CAAC,EAAE,UAAU,CAAA;IAChB,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,OAAO,CAAC,EAAE,UAAU,CAAA;CACrB;AAED,oCAAoC;AACpC,MAAM,WAAW,aAAa;IAC5B,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAA;IACf,6CAA6C;IAC7C,MAAM,EAAE,cAAc,CAAA;CACvB;AAID;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CA6B/B;AAcD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,UAAU,CA8BvE;AAID;;;GAGG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAkC7D;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EAAE,EACf,SAAS,EAAE,MAAM,GAChB,MAAM,CA0BR"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Middleware } from '@pyreon/server';
|
|
2
|
+
export interface CompressionConfig {
|
|
3
|
+
/** Minimum response size in bytes to compress. Default: `1024` (1KB) */
|
|
4
|
+
threshold?: number;
|
|
5
|
+
/** Encoding preference order. Default: `["gzip", "deflate"]` */
|
|
6
|
+
encodings?: ('gzip' | 'deflate')[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Compression middleware — compresses responses using gzip or deflate
|
|
10
|
+
* based on the client's Accept-Encoding header.
|
|
11
|
+
*
|
|
12
|
+
* Only compresses text-based content types (HTML, JSON, JS, CSS, XML, SVG).
|
|
13
|
+
* Skips responses below the size threshold and already-encoded responses.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* import { compressionMiddleware } from "@pyreon/zero/compression"
|
|
17
|
+
*
|
|
18
|
+
* compressionMiddleware() // gzip with 1KB threshold
|
|
19
|
+
* compressionMiddleware({ threshold: 512, encodings: ["gzip"] })
|
|
20
|
+
*/
|
|
21
|
+
export declare function compressionMiddleware(config?: CompressionConfig): Middleware;
|
|
22
|
+
/**
|
|
23
|
+
* Compress a Response body if it meets the criteria.
|
|
24
|
+
* Use this to post-process responses after the handler runs.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const response = await handler(request)
|
|
28
|
+
* const compressed = await compressResponse(response, 'gzip', 1024)
|
|
29
|
+
*/
|
|
30
|
+
export declare function compressResponse(response: Response, encoding: 'gzip' | 'deflate', threshold: number): Promise<Response>;
|
|
31
|
+
/** Check if a content type is compressible. Exported for testing. */
|
|
32
|
+
export declare function isCompressible(contentType: string): boolean;
|
|
33
|
+
//# sourceMappingURL=compression.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compression.d.ts","sourceRoot":"","sources":["../../src/compression.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,gBAAgB,CAAA;AAInE,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gEAAgE;IAChE,SAAS,CAAC,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAA;CACnC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,GAAE,iBAAsB,GAC7B,UAAU,CAeZ;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,QAAQ,CAAC,CA0BnB;AAWD,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE3D"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Middleware } from '@pyreon/server';
|
|
2
|
+
export interface CorsConfig {
|
|
3
|
+
/** Allowed origins. Use `"*"` for any origin. Default: `"*"` */
|
|
4
|
+
origin?: string | string[] | ((origin: string) => boolean);
|
|
5
|
+
/** Allowed HTTP methods. Default: `["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]` */
|
|
6
|
+
methods?: string[];
|
|
7
|
+
/** Allowed request headers. Default: `["Content-Type", "Authorization"]` */
|
|
8
|
+
allowedHeaders?: string[];
|
|
9
|
+
/** Headers exposed to the client. Default: `[]` */
|
|
10
|
+
exposedHeaders?: string[];
|
|
11
|
+
/** Allow credentials (cookies, auth headers). Default: `false` */
|
|
12
|
+
credentials?: boolean;
|
|
13
|
+
/** Preflight cache duration in seconds. Default: `86400` (24 hours) */
|
|
14
|
+
maxAge?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* CORS middleware — handles preflight requests and sets appropriate
|
|
18
|
+
* Access-Control headers on all responses.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* import { corsMiddleware } from "@pyreon/zero/cors"
|
|
22
|
+
*
|
|
23
|
+
* corsMiddleware({ origin: "https://example.com", credentials: true })
|
|
24
|
+
*
|
|
25
|
+
* // Allow any origin
|
|
26
|
+
* corsMiddleware({ origin: "*" })
|
|
27
|
+
*
|
|
28
|
+
* // Multiple origins
|
|
29
|
+
* corsMiddleware({ origin: ["https://app.com", "https://admin.com"] })
|
|
30
|
+
*/
|
|
31
|
+
export declare function corsMiddleware(config?: CorsConfig): Middleware;
|
|
32
|
+
//# sourceMappingURL=cors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../src/cors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,gBAAgB,CAAA;AAInE,MAAM,WAAW,UAAU;IACzB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,CAAA;IAC1D,4FAA4F;IAC5F,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,mDAAmD;IACnD,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,kEAAkE;IAClE,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAKD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,MAAM,GAAE,UAAe,GAAG,UAAU,CA+ClE"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RouteRecord } from '@pyreon/router';
|
|
2
2
|
import type { Middleware } from '@pyreon/server';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ApiRouteEntry } from './api-routes';
|
|
4
|
+
import type { RouteMiddlewareEntry, ZeroConfig } from './types';
|
|
4
5
|
export interface CreateServerOptions {
|
|
5
6
|
/** Route definitions. */
|
|
6
7
|
routes: RouteRecord[];
|
|
@@ -8,19 +9,26 @@ export interface CreateServerOptions {
|
|
|
8
9
|
config?: ZeroConfig;
|
|
9
10
|
/** Additional middleware. */
|
|
10
11
|
middleware?: Middleware[];
|
|
12
|
+
/** Per-route middleware from virtual:zero/route-middleware. */
|
|
13
|
+
routeMiddleware?: RouteMiddlewareEntry[];
|
|
14
|
+
/** API route entries from virtual:zero/api-routes. */
|
|
15
|
+
apiRoutes?: ApiRouteEntry[];
|
|
11
16
|
/** HTML template override. */
|
|
12
17
|
template?: string;
|
|
13
18
|
/** Client entry path. */
|
|
14
19
|
clientEntry?: string;
|
|
15
20
|
}
|
|
21
|
+
/** Simple URL pattern matcher supporting :param and :param* segments. */
|
|
22
|
+
export declare function matchPattern(pattern: string, path: string): boolean;
|
|
16
23
|
/**
|
|
17
24
|
* Create the SSR request handler for production.
|
|
18
25
|
*
|
|
19
26
|
* @example
|
|
20
27
|
* import { routes } from "virtual:zero/routes"
|
|
28
|
+
* import { routeMiddleware } from "virtual:zero/route-middleware"
|
|
21
29
|
* import { createServer } from "@pyreon/zero"
|
|
22
30
|
*
|
|
23
|
-
* export default createServer({ routes })
|
|
31
|
+
* export default createServer({ routes, routeMiddleware, apiRoutes })
|
|
24
32
|
*/
|
|
25
33
|
export declare function createServer(options: CreateServerOptions): (req: Request) => Promise<Response>;
|
|
26
34
|
//# sourceMappingURL=entry-server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-server.d.ts","sourceRoot":"","sources":["../../src/entry-server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACjD,OAAO,KAAK,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"entry-server.d.ts","sourceRoot":"","sources":["../../src/entry-server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACjD,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,gBAAgB,CAAA;AAEnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAGjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAI/D,MAAM,WAAW,mBAAmB;IAClC,yBAAyB;IACzB,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,mBAAmB;IACnB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,UAAU,EAAE,CAAA;IACzB,+DAA+D;IAC/D,eAAe,CAAC,EAAE,oBAAoB,EAAE,CAAA;IACxC,sDAAsD;IACtD,SAAS,CAAC,EAAE,aAAa,EAAE,CAAA;IAC3B,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAuBD,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAYnE;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,uCAgCxD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-overlay.d.ts","sourceRoot":"","sources":["../../src/error-overlay.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CA0FvD"}
|
package/lib/types/fs-router.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export declare function filePathToUrlPath(filePath: string): string;
|
|
|
25
25
|
* error/loading components, middleware, and meta from route module exports.
|
|
26
26
|
*/
|
|
27
27
|
export declare function generateRouteModule(files: string[], routesDir: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Generate a virtual module that maps URL patterns to their middleware exports.
|
|
30
|
+
* Used by the server entry to dispatch per-route middleware.
|
|
31
|
+
*/
|
|
32
|
+
export declare function generateMiddlewareModule(files: string[], routesDir: string): string;
|
|
28
33
|
/**
|
|
29
34
|
* Scan a directory for route files.
|
|
30
35
|
* Returns paths relative to the routes directory.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs-router.d.ts","sourceRoot":"","sources":["../../src/fs-router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AA6BpD;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EAAE,EACf,WAAW,GAAE,UAAkB,GAC9B,SAAS,EAAE,CAKb;AA0CD;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiC1D;AA0ED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EAAE,EACf,SAAS,EAAE,MAAM,GAChB,MAAM,CA6IR;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAoBzE"}
|
|
1
|
+
{"version":3,"file":"fs-router.d.ts","sourceRoot":"","sources":["../../src/fs-router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AA6BpD;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EAAE,EACf,WAAW,GAAE,UAAkB,GAC9B,SAAS,EAAE,CAKb;AA0CD;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiC1D;AA0ED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EAAE,EACf,SAAS,EAAE,MAAM,GAChB,MAAM,CA6IR;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,MAAM,EAAE,EACf,SAAS,EAAE,MAAM,GAChB,MAAM,CAuBR;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAoBzE"}
|
package/lib/types/image.d.ts
CHANGED
|
@@ -46,5 +46,5 @@ export interface ImageSource {
|
|
|
46
46
|
* // Manual usage
|
|
47
47
|
* <Image src="/hero.jpg" alt="Hero" width={1200} height={630} />
|
|
48
48
|
*/
|
|
49
|
-
export declare function Image(props: ImageProps):
|
|
49
|
+
export declare function Image(props: ImageProps): import("@pyreon/core").VNode;
|
|
50
50
|
//# sourceMappingURL=image.d.ts.map
|