page-preview 0.1.3

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,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Page Preview</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+ <script type="module" crossorigin src="/assets/index-s2ilP-oB.js"></script>
11
+ <link rel="stylesheet" crossorigin href="/assets/index-B4pZL0lz.css">
12
+ </head>
13
+ <body>
14
+ <div id="root"></div>
15
+ </body>
16
+ </html>
Binary file
@@ -0,0 +1,23 @@
1
+ # universal-expo Example
2
+
3
+ ## 1) Install
4
+
5
+ ```bash
6
+ pnpm add -D page-preview
7
+ ```
8
+
9
+ ## 2) Add stories
10
+
11
+ Create `next/dev/page-preview-stories.ts` and define `pagePreviewStories`.
12
+
13
+ ## 3) Register state adapters
14
+
15
+ Create `next/dev/register-preview-project-adapters.ts` and register your stores/clients.
16
+
17
+ ## 4) Run
18
+
19
+ ```bash
20
+ page-preview dev --stories next/dev/page-preview-stories.ts
21
+ ```
22
+
23
+ Then open `http://127.0.0.1:4100`.
@@ -0,0 +1,40 @@
1
+ import type { PagePreviewEntry } from "page-preview/lib";
2
+
3
+ export const pagePreviewStories: PagePreviewEntry[] = [
4
+ {
5
+ id: "create-artist",
6
+ group: "Sign in",
7
+ name: "Create artist",
8
+ target: {
9
+ path: "/sign-up",
10
+ variantQueryKey: "preview",
11
+ stateQueryKey: "__pp",
12
+ },
13
+ variants: [
14
+ {
15
+ id: "create-artist",
16
+ label: "Create idle",
17
+ state: {
18
+ zustand: [
19
+ {
20
+ storeId: "signup",
21
+ state: {
22
+ step: "instagram",
23
+ role: "artist",
24
+ chartmetricArtistId: "preview-artist-id",
25
+ name: "Preview Artist",
26
+ email: "preview@example.com",
27
+ localName: "Preview Artist",
28
+ },
29
+ },
30
+ ],
31
+ },
32
+ },
33
+ { id: "create-idle", label: "Create idle" },
34
+ {
35
+ id: "create-email-sent",
36
+ label: "Create email sent",
37
+ },
38
+ ],
39
+ },
40
+ ];
@@ -0,0 +1,9 @@
1
+ import { queryClient } from "@/lib/gql/query-client";
2
+ import { useSignUpStore } from "@/screens/auth/sign-up/sign-up-store";
3
+ import {
4
+ registerReactQueryClient,
5
+ registerZustandStore,
6
+ } from "@/dev/page-preview-bridge";
7
+
8
+ registerZustandStore("signup", useSignUpStore as unknown as Parameters<typeof registerZustandStore>[1]);
9
+ registerReactQueryClient("app", queryClient);
package/index.html ADDED
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Page Preview</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ <script type="module" src="/src/main.tsx"></script>
14
+ </body>
15
+ </html>
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "page-preview",
3
+ "version": "0.1.3",
4
+ "description": "Storybook-like page preview runtime for full-page state scenarios",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Yusang Park",
8
+ "homepage": "https://github.com/Yusang-park/page-preview#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Yusang-park/page-preview.git"
12
+ },
13
+ "keywords": [
14
+ "preview",
15
+ "storybook",
16
+ "react",
17
+ "vite",
18
+ "e2e"
19
+ ],
20
+ "files": [
21
+ "bin",
22
+ "dist",
23
+ "docs",
24
+ "examples",
25
+ "src",
26
+ "scripts",
27
+ "index.html",
28
+ "vite.config.ts",
29
+ "playwright.config.ts",
30
+ "README.md"
31
+ ],
32
+ "bin": {
33
+ "page-preview": "./bin/page-preview.mjs"
34
+ },
35
+ "scripts": {
36
+ "prepare:stories": "node ./scripts/prepare-stories.mjs",
37
+ "build": "pnpm run prepare:stories && vite build",
38
+ "dev": "pnpm run prepare:stories && vite --host 127.0.0.1 --port 4100 --strictPort",
39
+ "preview": "pnpm run prepare:stories && vite preview --host 127.0.0.1 --port 4100 --strictPort",
40
+ "e2e": "pnpm run prepare:stories && playwright test -c playwright.config.ts",
41
+ "e2e:headed": "pnpm run prepare:stories && playwright test -c playwright.config.ts --headed",
42
+ "e2e:ui": "pnpm run prepare:stories && playwright test -c playwright.config.ts --ui",
43
+ "prepublishOnly": "pnpm run build"
44
+ },
45
+ "exports": {
46
+ "./lib": "./src/lib/index.ts"
47
+ },
48
+ "dependencies": {
49
+ "@vitejs/plugin-react": "^5.0.4",
50
+ "playwright": "^1.58.2",
51
+ "react": "19.1.0",
52
+ "react-dom": "19.1.0",
53
+ "react-router-dom": "^7.9.4",
54
+ "vite": "^7.1.7"
55
+ },
56
+ "devDependencies": {
57
+ "@types/react": "~19.1.0",
58
+ "@types/react-dom": "^19.1.7",
59
+ "typescript": "~5.9.2"
60
+ }
61
+ }
@@ -0,0 +1,30 @@
1
+ import { defineConfig, devices } from "playwright/test";
2
+
3
+ const isCI = !!process.env.CI;
4
+
5
+ export default defineConfig({
6
+ testDir: "./e2e",
7
+ fullyParallel: true,
8
+ forbidOnly: isCI,
9
+ retries: isCI ? 2 : 0,
10
+ workers: isCI ? 1 : undefined,
11
+ reporter: isCI ? [["html"], ["list"]] : [["list"]],
12
+ use: {
13
+ baseURL: "http://127.0.0.1:4100",
14
+ trace: "on-first-retry",
15
+ },
16
+ projects: [
17
+ {
18
+ name: "chromium",
19
+ use: { ...devices["Desktop Chrome"] },
20
+ },
21
+ ],
22
+ webServer: [
23
+ {
24
+ command: "pnpm dev",
25
+ url: "http://127.0.0.1:4100",
26
+ timeout: 120000,
27
+ reuseExistingServer: false,
28
+ },
29
+ ],
30
+ });
@@ -0,0 +1,86 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const runtimeRoot = path.resolve(new URL("..", import.meta.url).pathname);
5
+ const injectedFile = path.resolve(runtimeRoot, "src/stories/injected.ts");
6
+ const defaultFile = path.resolve(runtimeRoot, "src/stories/default.ts");
7
+
8
+ const projectRoot = process.env.PAGE_PREVIEW_PROJECT_ROOT || process.cwd();
9
+ const envPath = process.env.PAGE_PREVIEW_STORIES_PATH;
10
+
11
+ const ignoredDirs = new Set([
12
+ ".git",
13
+ "node_modules",
14
+ ".next",
15
+ "dist",
16
+ "build",
17
+ "coverage",
18
+ "out",
19
+ "tmp",
20
+ ]);
21
+
22
+ const findPreviewFiles = (root) => {
23
+ const results = [];
24
+ const walk = (dir) => {
25
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
26
+ for (const entry of entries) {
27
+ const fullPath = path.join(dir, entry.name);
28
+ if (entry.isDirectory()) {
29
+ if (ignoredDirs.has(entry.name)) continue;
30
+ walk(fullPath);
31
+ continue;
32
+ }
33
+ if (entry.isFile() && entry.name.endsWith(".preview.ts")) {
34
+ results.push(fullPath);
35
+ }
36
+ }
37
+ };
38
+
39
+ walk(root);
40
+ return results.sort();
41
+ };
42
+
43
+ const makeImportPath = (targetFile) =>
44
+ path
45
+ .relative(path.dirname(injectedFile), targetFile)
46
+ .replace(/\\/g, "/")
47
+ .replace(/^(?!\.)/, "./")
48
+ .replace(/\.ts$/, "");
49
+
50
+ const writeSingleExport = (targetFile) => {
51
+ const importPath = makeImportPath(targetFile);
52
+ const content = `export { pagePreviewStories } from \"${importPath}\";\n`;
53
+ fs.writeFileSync(injectedFile, content, "utf8");
54
+ };
55
+
56
+ const writeMergedExports = (files) => {
57
+ const imports = files
58
+ .map((file, index) => `import { pagePreviewStories as stories${index} } from \"${makeImportPath(file)}\";`)
59
+ .join("\n");
60
+
61
+ const merged = `\nconst mergedStories = [${files.map((_, index) => `...stories${index}`).join(", ")}];\n\nexport const pagePreviewStories = mergedStories;\n`;
62
+
63
+ fs.writeFileSync(injectedFile, `${imports}${merged}`, "utf8");
64
+ };
65
+
66
+ if (envPath) {
67
+ const asAbsolute = path.isAbsolute(envPath) ? envPath : path.resolve(projectRoot, envPath);
68
+ const asRuntimeRelative = path.resolve(runtimeRoot, envPath);
69
+
70
+ if (fs.existsSync(asAbsolute)) {
71
+ writeSingleExport(asAbsolute);
72
+ process.exit(0);
73
+ }
74
+ if (fs.existsSync(asRuntimeRelative)) {
75
+ writeSingleExport(asRuntimeRelative);
76
+ process.exit(0);
77
+ }
78
+ }
79
+
80
+ const previewFiles = findPreviewFiles(projectRoot);
81
+
82
+ if (previewFiles.length > 0) {
83
+ writeMergedExports(previewFiles);
84
+ } else {
85
+ writeSingleExport(defaultFile);
86
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,294 @@
1
+ import React from "react";
2
+ import { Link, Navigate, Route, Routes, useLocation, useParams } from "react-router-dom";
3
+
4
+ import { encodePreviewState } from "./lib/state-codec";
5
+ import { pagePreviewStories } from "./stories";
6
+ import type { PagePreviewEntry } from "./lib/types";
7
+
8
+ // --- Icons ---
9
+
10
+ const LogoIcon = () => (
11
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
12
+ <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
13
+ <path d="M2 17L12 22L22 17" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
14
+ <path d="M2 12L12 17L22 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
15
+ </svg>
16
+ );
17
+
18
+ const BackIcon = () => (
19
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
20
+ <path d="M19 12H5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
21
+ <path d="M12 19L5 12L12 5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
22
+ </svg>
23
+ );
24
+
25
+ const ExternalLinkIcon = () => (
26
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
27
+ <path d="M18 13V19C18 19.5304 17.7893 20.0391 17.4142 20.4142C17.0391 20.7893 16.5304 21 16 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V8C3 7.46957 3.21071 6.96086 3.58579 6.58579C3.96086 6.21071 4.46957 6 5 6H11" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
28
+ <path d="M15 3H21V9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
29
+ <path d="M10 14L21 3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
30
+ </svg>
31
+ );
32
+
33
+ // --- Logic ---
34
+
35
+ const groupEntries = (entries: PagePreviewEntry[]) =>
36
+ entries.reduce<Record<string, PagePreviewEntry[]>>((acc, entry) => {
37
+ const key = entry.group ?? "Ungrouped";
38
+ if (!acc[key]) acc[key] = [];
39
+ acc[key].push(entry);
40
+ return acc;
41
+ }, {});
42
+
43
+ const buildTargetUrl = (
44
+ entry: PagePreviewEntry,
45
+ variantId?: string,
46
+ options?: { hideToolbar?: boolean },
47
+ ) => {
48
+ const origin = entry.target.origin ?? "http://127.0.0.1:4000";
49
+ const url = new URL(entry.target.path, origin);
50
+ if (entry.target.variantQueryKey) {
51
+ const resolvedVariant = variantId ?? entry.variants[0]?.id;
52
+ if (resolvedVariant) {
53
+ url.searchParams.set(entry.target.variantQueryKey, resolvedVariant);
54
+ }
55
+ }
56
+ const selectedVariant =
57
+ entry.variants.find((variant) => variant.id === variantId) ?? entry.variants[0];
58
+ const stateKey = entry.target.stateQueryKey ?? "__pp";
59
+ if (selectedVariant?.state) {
60
+ url.searchParams.set(stateKey, encodePreviewState(selectedVariant.state));
61
+ } else {
62
+ url.searchParams.delete(stateKey);
63
+ }
64
+ if (options?.hideToolbar) {
65
+ url.searchParams.set("__pp_toolbar", "0");
66
+ }
67
+ return url.toString();
68
+ };
69
+
70
+ // --- Components ---
71
+
72
+ const ScaledPreviewFrame = ({
73
+ src,
74
+ title,
75
+ compact = false,
76
+ }: {
77
+ src: string;
78
+ title: string;
79
+ compact?: boolean;
80
+ }) => {
81
+ const shellRef = React.useRef<HTMLDivElement | null>(null);
82
+ const [scale, setScale] = React.useState(1);
83
+
84
+ React.useEffect(() => {
85
+ const shell = shellRef.current;
86
+ if (!shell) return;
87
+ const observer = new ResizeObserver((entries) => {
88
+ const target = entries[0];
89
+ if (!target) return;
90
+ const width = target.contentRect.width;
91
+ const height = target.contentRect.height;
92
+ // 1920x1280 is the base resolution for scaling
93
+ const nextScale = Math.min(width / 1920, height / 1280);
94
+ setScale(nextScale);
95
+ });
96
+ observer.observe(shell);
97
+ return () => observer.disconnect();
98
+ }, []);
99
+
100
+ return (
101
+ <div ref={shellRef} className={`pp-canvas-shell ${compact ? "pp-canvas-shell-compact" : ""}`}>
102
+ <div
103
+ className="pp-canvas-inner"
104
+ style={{ transform: `translate(-50%, -50%) scale(${scale})` }}
105
+ >
106
+ <iframe className="pp-viewer-frame" src={src} title={title} />
107
+ </div>
108
+ </div>
109
+ );
110
+ };
111
+
112
+ const PreviewGrid = () => {
113
+ const grouped = groupEntries(pagePreviewStories);
114
+ return (
115
+ <div className="pp-main">
116
+ <div className="pp-shell">
117
+ <header className="pp-header">
118
+ <div className="pp-kicker">Runtime Environment</div>
119
+ <h1 className="pp-title">Page Preview Gallery</h1>
120
+ <p className="pp-description">
121
+ Explore and verify page states in an isolated React runtime.
122
+ </p>
123
+ </header>
124
+ {Object.keys(grouped).length === 0 ? (
125
+ <section className="pp-group">
126
+ <div className="pp-group-head">
127
+ <h2 className="pp-group-title">No previews registered</h2>
128
+ </div>
129
+ <p className="pp-description" style={{ marginTop: 12 }}>
130
+ Pass a stories file with `page-preview dev --stories path/to/page-preview-stories.ts`.
131
+ </p>
132
+ </section>
133
+ ) : null}
134
+ {Object.entries(grouped).map(([groupName, entries]) => (
135
+ <section key={groupName} className="pp-group">
136
+ <div className="pp-group-head">
137
+ <h2 className="pp-group-title">{groupName}</h2>
138
+ </div>
139
+ <div className="pp-grid">
140
+ {entries.map((entry) => (
141
+ <article key={entry.id} className="pp-card">
142
+ <Link to={`/${entry.id}`} className="pp-frame-wrap" style={{ display: 'block', textDecoration: 'none' }}>
143
+ <div style={{ pointerEvents: 'none' }}>
144
+ <ScaledPreviewFrame
145
+ title={`${entry.id}-preview`}
146
+ src={buildTargetUrl(entry, entry.variants[0]?.id, { hideToolbar: true })}
147
+ compact
148
+ />
149
+ </div>
150
+ </Link>
151
+ <Link to={`/${entry.id}`} style={{ textDecoration: 'none' }}>
152
+ <h3 className="pp-card-title">{entry.name ?? entry.id}</h3>
153
+ </Link>
154
+ <div className="pp-chip-row">
155
+ {entry.variants.map((v) => (
156
+ <a className="pp-chip" href={buildTargetUrl(entry, v.id)} key={v.id}>
157
+ {v.id}
158
+ </a>
159
+ ))}
160
+ </div>
161
+ <div className="pp-action-row">
162
+ <Link className="pp-open" to={`/${entry.id}`}>
163
+ View Details
164
+ <span style={{ marginLeft: 8 }}><ExternalLinkIcon /></span>
165
+ </Link>
166
+ </div>
167
+ </article>
168
+ ))}
169
+ </div>
170
+ </section>
171
+ ))}
172
+ </div>
173
+ </div>
174
+ );
175
+ };
176
+
177
+ const PageViewer = ({ entry }: { entry: PagePreviewEntry }) => {
178
+ const location = useLocation();
179
+ const params = new URLSearchParams(location.search);
180
+ const selected = params.get("variant");
181
+ const [highlightId, setHighlightId] = React.useState<string | null>(null);
182
+
183
+ React.useEffect(() => {
184
+ if (selected) {
185
+ setHighlightId(selected);
186
+ const timer = setTimeout(() => setHighlightId(null), 2000);
187
+
188
+ // Scroll to the selected variant
189
+ const element = document.getElementById(`variant-${selected}`);
190
+ if (element) {
191
+ element.scrollIntoView({ behavior: "smooth", block: "center" });
192
+ }
193
+
194
+ return () => clearTimeout(timer);
195
+ }
196
+ }, [selected]);
197
+
198
+ return (
199
+ <div className="pp-main pp-viewer-root">
200
+ <div className="pp-viewer-toolbar">
201
+ <div className="pp-viewer-top">
202
+ <div className="pp-viewer-back-wrap">
203
+ <Link className="pp-toolbar-back" to="/" aria-label="Back to gallery">
204
+ <BackIcon />
205
+ </Link>
206
+ </div>
207
+ <div className="pp-viewer-title">
208
+ <strong>{entry.group ?? "Preview"}</strong>
209
+ <span>{entry.name ?? entry.id}</span>
210
+ </div>
211
+ <div style={{ width: 40 }} /> {/* Spacer for centering title */}
212
+ </div>
213
+ </div>
214
+
215
+ <div className="pp-viewer-shell">
216
+ <div className="pp-viewer-grid">
217
+ {entry.variants.map((v) => (
218
+ <article
219
+ key={v.id}
220
+ id={`variant-${v.id}`}
221
+ className={`pp-preview-card ${v.id === highlightId ? "pp-preview-card-active" : ""}`}
222
+ >
223
+ <ScaledPreviewFrame
224
+ title={`${entry.id}-${v.id}`}
225
+ src={buildTargetUrl(entry, v.id)}
226
+ />
227
+ <div className="pp-preview-card-footer">
228
+ <h3 className="pp-preview-card-title">{v.id}</h3>
229
+ <a className="pp-open pp-preview-open-link" href={buildTargetUrl(entry, v.id)}>
230
+ Open page
231
+ </a>
232
+ </div>
233
+ </article>
234
+ ))}
235
+ </div>
236
+ </div>
237
+ </div>
238
+ );
239
+ };
240
+
241
+ const PreviewPageRoute = () => {
242
+ const params = useParams();
243
+ const pageId = params["*"] ? `${params.pageId}/${params["*"]}` : params.pageId;
244
+ const entry = pagePreviewStories.find((s) => s.id === pageId);
245
+ if (!entry) return <Navigate to="/" replace />;
246
+ return <PageViewer entry={entry} />;
247
+ };
248
+
249
+ const Sidebar = ({ activePageId }: { activePageId?: string }) => {
250
+ const grouped = groupEntries(pagePreviewStories);
251
+ return (
252
+ <aside className="pp-sidebar">
253
+ <div className="pp-sidebar-head">
254
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12, color: 'var(--text-primary)' }}>
255
+ <LogoIcon />
256
+ <span style={{ fontWeight: 600, fontSize: '1rem' }}>Page Preview</span>
257
+ </div>
258
+ </div>
259
+ <nav className="pp-sidebar-nav">
260
+ {Object.entries(grouped).map(([groupName, entries]) => (
261
+ <div key={groupName}>
262
+ <h3 className="pp-sidebar-group-title">{groupName}</h3>
263
+ <div className="pp-sidebar-links">
264
+ {entries.map((entry) => (
265
+ <Link
266
+ key={entry.id}
267
+ className={`pp-sidebar-link ${activePageId === entry.id ? "pp-sidebar-link-active" : ""}`}
268
+ to={`/${entry.id}`}
269
+ >
270
+ {entry.name ?? entry.id}
271
+ </Link>
272
+ ))}
273
+ </div>
274
+ </div>
275
+ ))}
276
+ </nav>
277
+ </aside>
278
+ );
279
+ };
280
+
281
+ export default function App() {
282
+ const location = useLocation();
283
+ const pageId = location.pathname.substring(1) || undefined;
284
+ return (
285
+ <div className="pp-root">
286
+ <Sidebar activePageId={pageId} />
287
+ <Routes>
288
+ <Route path="/" element={<PreviewGrid />} />
289
+ <Route path="/:pageId/*" element={<PreviewPageRoute />} />
290
+ <Route path="*" element={<Navigate to="/" replace />} />
291
+ </Routes>
292
+ </div>
293
+ );
294
+ }
@@ -0,0 +1,16 @@
1
+ import type { PagePreviewEntry, PagePreviewVariant } from "./types";
2
+
3
+ export const buildVariantViewerPath = (
4
+ entry: Pick<PagePreviewEntry, "id">,
5
+ variant: Pick<PagePreviewVariant, "id">,
6
+ ) => `/preview/${entry.id}?variant=${variant.id}`;
7
+
8
+ export const buildVariantRenderPath = (
9
+ entry: Pick<PagePreviewEntry, "id">,
10
+ variant: Pick<PagePreviewVariant, "id">,
11
+ ) => `/preview/render/${entry.id}/${variant.id}`;
12
+
13
+ export const findPagePreviewEntry = (
14
+ entries: PagePreviewEntry[],
15
+ pageId: string,
16
+ ) => entries.find((entry) => entry.id === pageId);
@@ -0,0 +1,4 @@
1
+ export * from "./helpers";
2
+ export * from "./preview-bridge";
3
+ export * from "./state-codec";
4
+ export * from "./types";