@yaakdoc/react 0.0.0-stage → 0.1.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.
@@ -0,0 +1,164 @@
1
+ import { createContext, createElement, use, useCallback, useEffect, useState } from "react";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/island.tsx
4
+ /**
5
+ * Provided by the server renderer. Islands are rendered as isolated roots (exactly like they are
6
+ * hydrated on the client), so `useId()` and friends produce matching ids.
7
+ * @internal
8
+ */
9
+ const IslandRendererContext = createContext(null);
10
+ /**
11
+ * Turns a component into an island: it is rendered to HTML on the server like any other component,
12
+ * and hydrated on the client (only this component, not the whole page).
13
+ *
14
+ * Islands are hydrated in isolation: their props must be JSON-serializable and they cannot use
15
+ * yaakdoc context hooks. Register islands on the client with `hydrateIslands()`.
16
+ */
17
+ function createIsland(name, Component) {
18
+ function Island(props) {
19
+ const renderer = use(IslandRendererContext);
20
+ const common = {
21
+ "data-island": name,
22
+ "data-props": JSON.stringify(props),
23
+ style: { display: "contents" }
24
+ };
25
+ if (renderer) {
26
+ const prefix = renderer.next();
27
+ return /* @__PURE__ */ jsx("yd-island", {
28
+ ...common,
29
+ "data-prefix": prefix,
30
+ dangerouslySetInnerHTML: { __html: renderer.render(createElement(Component, props), prefix) }
31
+ });
32
+ }
33
+ return /* @__PURE__ */ jsx("yd-island", {
34
+ ...common,
35
+ children: /* @__PURE__ */ jsx(Component, { ...props })
36
+ });
37
+ }
38
+ Island.displayName = `Island(${name})`;
39
+ return Object.assign(Island, {
40
+ islandName: name,
41
+ Component
42
+ });
43
+ }
44
+ //#endregion
45
+ //#region src/islands/browser.ts
46
+ /** Browser helpers shared by the islands. */
47
+ /** `localStorage` access that never throws (private mode, blocked storage...). */
48
+ const storage = {
49
+ get(key) {
50
+ try {
51
+ return localStorage.getItem(key);
52
+ } catch {
53
+ return null;
54
+ }
55
+ },
56
+ set(key, value) {
57
+ try {
58
+ localStorage.setItem(key, value);
59
+ } catch {}
60
+ }
61
+ };
62
+ /** Resolves a (possibly relative) URL against the current page. */
63
+ const resolveUrl = (url) => new URL(url, window.location.href).href;
64
+ //#endregion
65
+ //#region src/islands/environment.tsx
66
+ const ENVIRONMENT_STORAGE_KEY = "yaakdoc-environment";
67
+ const ENVIRONMENT_EVENT = "yaakdoc:environment";
68
+ /**
69
+ * Shows the content variants of one environment. Variants are elements with
70
+ * `data-yd-env="<environment id>"`; `<html data-environment>` drives the CSS (see
71
+ * {@link environmentCss}) and the `hidden` attributes are kept in sync for accessibility.
72
+ */
73
+ function applyEnvironment(id) {
74
+ document.documentElement.dataset.environment = id;
75
+ for (const element of document.querySelectorAll("[data-yd-env]")) element.hidden = element.dataset.ydEnv !== id;
76
+ storage.set(ENVIRONMENT_STORAGE_KEY, id);
77
+ window.dispatchEvent(new CustomEvent(ENVIRONMENT_EVENT, { detail: id }));
78
+ }
79
+ /** Headless environment switcher state, synced across switchers and remembered. */
80
+ function useEnvironment(environments, defaultId) {
81
+ const [selected, setSelected] = useState(defaultId);
82
+ useEffect(() => {
83
+ const onChange = (event) => setSelected(event.detail);
84
+ window.addEventListener(ENVIRONMENT_EVENT, onChange);
85
+ const current = document.documentElement.dataset.environment;
86
+ if (current && current !== defaultId && environments.some((env) => env.id === current)) applyEnvironment(current);
87
+ return () => window.removeEventListener(ENVIRONMENT_EVENT, onChange);
88
+ }, [environments, defaultId]);
89
+ return {
90
+ selected,
91
+ select: useCallback((id) => {
92
+ if (environments.some((env) => env.id === id)) applyEnvironment(id);
93
+ }, [environments]),
94
+ current: environments.find((env) => env.id === selected)
95
+ };
96
+ }
97
+ /** Inline <head> script: applies the remembered environment before first paint. */
98
+ function environmentScript(ids) {
99
+ return `(function(){try{var e=localStorage.getItem(${JSON.stringify(ENVIRONMENT_STORAGE_KEY)});if(e&&${JSON.stringify(ids)}.indexOf(e)>=0)document.documentElement.dataset.environment=e}catch(_){}})();`;
100
+ }
101
+ /** CSS that shows the variants of the environment set on <html> (falls back to `hidden`). */
102
+ function environmentCss(ids) {
103
+ return ["[data-yd-env]{display:contents}[data-yd-env][hidden]{display:none}", ...ids.map((id) => {
104
+ const value = JSON.stringify(id);
105
+ return `html[data-environment=${value}] [data-yd-env]:not([data-yd-env=${value}]){display:none}html[data-environment=${value}] [data-yd-env=${value}]{display:contents}`;
106
+ })].join("");
107
+ }
108
+ function EnvironmentSwitcherImpl({ environments, defaultId, className, label = "Environment" }) {
109
+ const { selected, select } = useEnvironment(environments, defaultId);
110
+ return /* @__PURE__ */ jsx("select", {
111
+ "data-yd": "environment-switcher",
112
+ className,
113
+ "aria-label": label,
114
+ value: selected,
115
+ onChange: (event) => select(event.target.value),
116
+ children: environments.map((env) => /* @__PURE__ */ jsx("option", {
117
+ value: env.id,
118
+ children: env.name
119
+ }, env.id))
120
+ });
121
+ }
122
+ /** Unstyled environment switcher (native select). */
123
+ const EnvironmentSwitcher = createIsland("yd:environment-switcher", EnvironmentSwitcherImpl);
124
+ //#endregion
125
+ //#region src/islands/run-in-yaak.tsx
126
+ /** Official "Run in Yaak" badge image. */
127
+ const RUN_IN_YAAK_BADGE = "https://yaak.app/static/button.svg";
128
+ /** Link that opens Yaak and prompts to import the export/spec at `dataUrl` (must be absolute). */
129
+ function runInYaakUrl(name, dataUrl) {
130
+ return `https://yaak.app/button/run?${new URLSearchParams({
131
+ name,
132
+ url: dataUrl
133
+ })}`;
134
+ }
135
+ /**
136
+ * "Run in Yaak" link for an export URL. Relative URLs (sites without a configured public URL) are
137
+ * made absolute on the client, after hydration.
138
+ */
139
+ function useRunInYaakHref(name, dataUrl) {
140
+ const [href, setHref] = useState(() => runInYaakUrl(name, dataUrl));
141
+ useEffect(() => {
142
+ setHref(runInYaakUrl(name, resolveUrl(dataUrl)));
143
+ }, [name, dataUrl]);
144
+ return href;
145
+ }
146
+ function RunInYaakLinkImpl({ name, dataUrl, badge = false, label = "Run in Yaak", className }) {
147
+ const href = useRunInYaakHref(name, dataUrl);
148
+ return /* @__PURE__ */ jsx("a", {
149
+ href,
150
+ "data-yd": "run-in-yaak",
151
+ className,
152
+ target: "_blank",
153
+ rel: "noopener",
154
+ children: badge ? /* @__PURE__ */ jsx("img", {
155
+ src: RUN_IN_YAAK_BADGE,
156
+ alt: label,
157
+ height: 32
158
+ }) : label
159
+ });
160
+ }
161
+ /** Unstyled "Run in Yaak" link (or the official badge with `badge`). */
162
+ const RunInYaakLink = createIsland("yd:run-in-yaak", RunInYaakLinkImpl);
163
+ //#endregion
164
+ export { ENVIRONMENT_STORAGE_KEY as a, environmentCss as c, resolveUrl as d, storage as f, useRunInYaakHref as i, environmentScript as l, createIsland as m, RunInYaakLink as n, EnvironmentSwitcher as o, IslandRendererContext as p, runInYaakUrl as r, applyEnvironment as s, RUN_IN_YAAK_BADGE as t, useEnvironment as u };
@@ -0,0 +1,26 @@
1
+ import { _ as ThemeRuntime, l as PageData, p as SiteData, t as Assets } from "./types-BY2lOvlK.js";
2
+ import { PreparedDocument } from "@yaakdoc/core";
3
+ import { ReactNode } from "react";
4
+ //#region src/document.d.ts
5
+ type DocumentProps = {
6
+ site: SiteData;
7
+ page: PageData;
8
+ assets: Assets;
9
+ ThemeHead?: ThemeRuntime["Head"];
10
+ children: ReactNode;
11
+ };
12
+ /** The HTML document shell: meta tags, color mode script, assets, branding. */
13
+ export declare function Document({ site, page, assets, ThemeHead, children }: DocumentProps): import("react").JSX.Element;
14
+ //#endregion
15
+ //#region src/server.d.ts
16
+ export type RenderPageOptions = {
17
+ theme: ThemeRuntime;
18
+ document: PreparedDocument;
19
+ site: SiteData;
20
+ page: PageData;
21
+ assets: Assets;
22
+ };
23
+ /** Renders a full HTML page (static: no hydration of the page itself, islands hydrate separately). */
24
+ export declare function renderPage({ theme, document, site, page, assets }: RenderPageOptions): Promise<string>;
25
+ //#endregion
26
+ export type { DocumentProps };
package/dist/server.js ADDED
@@ -0,0 +1,139 @@
1
+ import { b as useDocument, p as ComponentsProvider, t as BrandingStyles, v as toAbsoluteUrl, y as YaakdocProvider } from "./site-DyK7ZcqQ.js";
2
+ import { c as environmentCss, l as environmentScript, p as IslandRendererContext } from "./run-in-yaak-CMMcDS7S.js";
3
+ import { i as colorModeScript } from "./color-mode-B-vJCVkP.js";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { renderToString } from "react-dom/server";
6
+ import { prerender } from "react-dom/static";
7
+ //#region src/document.tsx
8
+ const absolute = (site, path) => toAbsoluteUrl(site, path) ?? path;
9
+ /** The HTML document shell: meta tags, color mode script, assets, branding. */
10
+ function Document({ site, page, assets, ThemeHead, children }) {
11
+ const title = page.title && page.title !== site.title ? `${page.title} · ${site.title}` : site.title;
12
+ const description = page.description || site.description;
13
+ const environmentIds = useDocument().environments.map((env) => env.id);
14
+ return /* @__PURE__ */ jsxs("html", {
15
+ lang: "en",
16
+ "data-color-mode": site.defaultColorMode,
17
+ suppressHydrationWarning: true,
18
+ children: [/* @__PURE__ */ jsxs("head", { children: [
19
+ /* @__PURE__ */ jsx("meta", { charSet: "utf-8" }),
20
+ /* @__PURE__ */ jsx("meta", {
21
+ name: "viewport",
22
+ content: "width=device-width, initial-scale=1"
23
+ }),
24
+ /* @__PURE__ */ jsx("title", { children: title }),
25
+ description ? /* @__PURE__ */ jsx("meta", {
26
+ name: "description",
27
+ content: description
28
+ }) : null,
29
+ /* @__PURE__ */ jsx("meta", {
30
+ name: "generator",
31
+ content: site.generator
32
+ }),
33
+ /* @__PURE__ */ jsx("meta", {
34
+ property: "og:type",
35
+ content: "website"
36
+ }),
37
+ /* @__PURE__ */ jsx("meta", {
38
+ property: "og:title",
39
+ content: title
40
+ }),
41
+ description ? /* @__PURE__ */ jsx("meta", {
42
+ property: "og:description",
43
+ content: description
44
+ }) : null,
45
+ site.url ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("link", {
46
+ rel: "canonical",
47
+ href: absolute(site, page.path)
48
+ }), /* @__PURE__ */ jsx("meta", {
49
+ property: "og:url",
50
+ content: absolute(site, page.path)
51
+ })] }) : null,
52
+ page.kind === "not-found" ? /* @__PURE__ */ jsx("meta", {
53
+ name: "robots",
54
+ content: "noindex"
55
+ }) : null,
56
+ /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: colorModeScript(site.defaultColorMode) } }),
57
+ environmentIds.length > 1 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: environmentScript(environmentIds) } }), /* @__PURE__ */ jsx("style", {
58
+ "data-yd": "environments",
59
+ dangerouslySetInnerHTML: { __html: environmentCss(environmentIds) }
60
+ })] }) : null,
61
+ site.favicon ? /* @__PURE__ */ jsx("link", {
62
+ rel: "icon",
63
+ href: site.favicon
64
+ }) : null,
65
+ site.fonts.stylesheet ? /* @__PURE__ */ jsx("link", {
66
+ rel: "stylesheet",
67
+ href: site.fonts.stylesheet
68
+ }) : null,
69
+ page.markdownUrl ? /* @__PURE__ */ jsx("link", {
70
+ rel: "alternate",
71
+ type: "text/markdown",
72
+ href: page.markdownUrl,
73
+ title: "Markdown"
74
+ }) : null,
75
+ site.llms.txtUrl ? /* @__PURE__ */ jsx("link", {
76
+ rel: "alternate",
77
+ type: "text/plain",
78
+ href: site.llms.txtUrl,
79
+ title: "llms.txt"
80
+ }) : null,
81
+ assets.modulePreloads.map((href) => /* @__PURE__ */ jsx("link", {
82
+ rel: "modulepreload",
83
+ href
84
+ }, href)),
85
+ assets.styles.map((href) => /* @__PURE__ */ jsx("link", {
86
+ rel: "stylesheet",
87
+ href
88
+ }, href)),
89
+ assets.inlineStyles.map((style) => /* @__PURE__ */ jsx("style", {
90
+ "data-vite-dev-id": style.id,
91
+ dangerouslySetInnerHTML: { __html: style.css }
92
+ }, style.id)),
93
+ /* @__PURE__ */ jsx(BrandingStyles, {}),
94
+ ThemeHead ? /* @__PURE__ */ jsx(ThemeHead, { page }) : null,
95
+ site.head.map(({ tag: Tag, attrs, content }) => /* @__PURE__ */ jsx(Tag, {
96
+ ...attrs,
97
+ ...content ? { dangerouslySetInnerHTML: { __html: content } } : {}
98
+ }, `${Tag}:${JSON.stringify(attrs)}:${content ?? ""}`))
99
+ ] }), /* @__PURE__ */ jsxs("body", { children: [children, assets.scripts.map((src) => /* @__PURE__ */ jsx("script", {
100
+ type: "module",
101
+ src
102
+ }, src))] })]
103
+ });
104
+ }
105
+ //#endregion
106
+ //#region src/server.tsx
107
+ /** Renders a full HTML page (static: no hydration of the page itself, islands hydrate separately). */
108
+ async function renderPage({ theme, document, site, page, assets }) {
109
+ const { Layout } = theme;
110
+ let counter = 0;
111
+ const element = /* @__PURE__ */ jsx(IslandRendererContext, {
112
+ value: {
113
+ render: (island, identifierPrefix) => renderToString(island, { identifierPrefix }),
114
+ next: () => `i${(counter++).toString(36)}-`
115
+ },
116
+ children: /* @__PURE__ */ jsx(YaakdocProvider, {
117
+ document,
118
+ site,
119
+ page,
120
+ children: /* @__PURE__ */ jsx(ComponentsProvider, {
121
+ components: theme.components,
122
+ children: /* @__PURE__ */ jsx(Document, {
123
+ site,
124
+ page,
125
+ assets,
126
+ ThemeHead: theme.Head,
127
+ children: /* @__PURE__ */ jsx(Layout, { page })
128
+ })
129
+ })
130
+ })
131
+ });
132
+ const { prelude } = await prerender(element, { onError(error) {
133
+ console.error("[yaakdoc] Render error:", error);
134
+ } });
135
+ const html = await new Response(prelude).text();
136
+ return html.startsWith("<!DOCTYPE") ? html : `<!DOCTYPE html>${html}`;
137
+ }
138
+ //#endregion
139
+ export { Document, renderPage };