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.
- package/README.md +142 -0
- package/bin/page-preview.mjs +75 -0
- package/dist/assets/index-B4pZL0lz.css +1 -0
- package/dist/assets/index-s2ilP-oB.js +11 -0
- package/dist/index.html +16 -0
- package/docs/screenshots/gallery.png +0 -0
- package/examples/universal-expo/README.md +23 -0
- package/examples/universal-expo/next/dev/page-preview-stories.ts +40 -0
- package/examples/universal-expo/next/dev/register-preview-project-adapters.ts +9 -0
- package/index.html +15 -0
- package/package.json +61 -0
- package/playwright.config.ts +30 -0
- package/scripts/prepare-stories.mjs +86 -0
- package/src/App.tsx +294 -0
- package/src/lib/helpers.ts +16 -0
- package/src/lib/index.ts +4 -0
- package/src/lib/preview-bridge.ts +100 -0
- package/src/lib/state-codec.ts +26 -0
- package/src/lib/types.ts +49 -0
- package/src/main.tsx +14 -0
- package/src/stories/default.ts +3 -0
- package/src/stories/index.ts +4 -0
- package/src/stories/injected.ts +1 -0
- package/src/styles/app.css +480 -0
- package/vite.config.ts +6 -0
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# page-preview
|
|
2
|
+
|
|
3
|
+
Storybook-like **page runtime preview** for full pages and hidden UI branches.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Component story tools are great for isolated UI, but they do not always cover real page branching (auth gates, async states, seeded stores, deep paths). `page-preview` runs a dedicated preview runtime on `:4100` and lets you define page variants as stories.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add -D page-preview
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Zero-config discovery (`*.preview.ts`)
|
|
18
|
+
|
|
19
|
+
By default, `page-preview` scans your project root and automatically merges every `*.preview.ts` file.
|
|
20
|
+
|
|
21
|
+
You can run without any stories argument:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
page-preview dev
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This means in app projects you can keep only one script:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"scripts": {
|
|
32
|
+
"page-preview": "pnpm exec page-preview dev"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then run:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pnpm page-preview
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Story file format
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import type { PagePreviewEntry } from "page-preview/lib";
|
|
47
|
+
|
|
48
|
+
export const pagePreviewStories: PagePreviewEntry[] = [
|
|
49
|
+
{
|
|
50
|
+
id: "create-artist",
|
|
51
|
+
group: "Sign in",
|
|
52
|
+
name: "Create artist",
|
|
53
|
+
title: "Create artist",
|
|
54
|
+
description: "Artist onboarding states",
|
|
55
|
+
target: {
|
|
56
|
+
path: "/sign-up",
|
|
57
|
+
variantQueryKey: "preview",
|
|
58
|
+
stateQueryKey: "__pp",
|
|
59
|
+
},
|
|
60
|
+
variants: [
|
|
61
|
+
{
|
|
62
|
+
id: "create-artist",
|
|
63
|
+
label: "Artist selected",
|
|
64
|
+
state: {
|
|
65
|
+
zustand: [{ storeId: "signup", state: { step: "instagram" } }],
|
|
66
|
+
reactQuery: [{ queryKey: ["seed"], data: { ok: true } }],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{ id: "create-idle", label: "Idle" },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## State plugin injection (Zustand / Redux / Context / React Query)
|
|
76
|
+
|
|
77
|
+
Use `createPreviewBridge` from the library and register your state containers once.
|
|
78
|
+
|
|
79
|
+
### 1) Create bridge instance
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { createPreviewBridge } from "page-preview/lib";
|
|
83
|
+
|
|
84
|
+
export const previewBridge = createPreviewBridge({
|
|
85
|
+
queryKey: "__pp", // default
|
|
86
|
+
developmentOnly: true, // default
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 2) Register stores/clients
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { queryClient } from "@/lib/gql/query-client";
|
|
94
|
+
import { useSignUpStore } from "@/screens/auth/sign-up/sign-up-store";
|
|
95
|
+
import { previewBridge } from "./preview-bridge";
|
|
96
|
+
|
|
97
|
+
previewBridge.registerZustandStore("signup", useSignUpStore as unknown as { setState: (state: Record<string, unknown>) => void });
|
|
98
|
+
previewBridge.registerReactQueryClient("app", queryClient);
|
|
99
|
+
|
|
100
|
+
// Optional providers:
|
|
101
|
+
// previewBridge.registerReduxStore("app", reduxStore);
|
|
102
|
+
// previewBridge.registerContextSetter("my-context", setContextValue);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 3) Apply snapshot from URL query once on app startup
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { previewBridge } from "@/dev/preview-bridge";
|
|
109
|
+
|
|
110
|
+
if (typeof window !== "undefined") {
|
|
111
|
+
previewBridge.applyFromSearch(window.location.search);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 4) Emit state from stories
|
|
116
|
+
|
|
117
|
+
`state` in each variant supports:
|
|
118
|
+
|
|
119
|
+
- `zustand: [{ storeId, state }]`
|
|
120
|
+
- `redux: [{ storeId, action }]`
|
|
121
|
+
- `context: [{ contextId, value }]`
|
|
122
|
+
- `reactQuery: [{ queryKey, data }]`
|
|
123
|
+
|
|
124
|
+
## Commands
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
page-preview dev
|
|
128
|
+
page-preview build
|
|
129
|
+
page-preview preview
|
|
130
|
+
page-preview e2e
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Optional single-file mode (override auto discovery):
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
page-preview dev --stories next/dev/custom-stories.ts
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Examples
|
|
140
|
+
|
|
141
|
+
See `examples/universal-expo`.
|
|
142
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
9
|
+
const requireFromPackage = createRequire(path.join(packageRoot, "package.json"));
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const command = args[0] ?? "dev";
|
|
13
|
+
const projectRoot = process.cwd();
|
|
14
|
+
|
|
15
|
+
const getOption = (name) => {
|
|
16
|
+
const idx = args.findIndex((arg) => arg === `--${name}`);
|
|
17
|
+
if (idx === -1) return undefined;
|
|
18
|
+
return args[idx + 1];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const storiesPath = getOption("stories");
|
|
22
|
+
if (storiesPath) {
|
|
23
|
+
process.env.PAGE_PREVIEW_STORIES_PATH = path.resolve(projectRoot, storiesPath);
|
|
24
|
+
}
|
|
25
|
+
process.env.PAGE_PREVIEW_PROJECT_ROOT = projectRoot;
|
|
26
|
+
|
|
27
|
+
const nodeBin = process.execPath;
|
|
28
|
+
const vitePackageRoot = path.dirname(requireFromPackage.resolve("vite/package.json"));
|
|
29
|
+
const playwrightPackageRoot = path.dirname(
|
|
30
|
+
requireFromPackage.resolve("playwright/package.json"),
|
|
31
|
+
);
|
|
32
|
+
const viteBin = path.join(vitePackageRoot, "bin", "vite.js");
|
|
33
|
+
const playwrightBin = path.join(playwrightPackageRoot, "cli.js");
|
|
34
|
+
|
|
35
|
+
const run = (bin, runArgs) =>
|
|
36
|
+
new Promise((resolve) => {
|
|
37
|
+
const child = spawn(nodeBin, [bin, ...runArgs], {
|
|
38
|
+
cwd: packageRoot,
|
|
39
|
+
stdio: "inherit",
|
|
40
|
+
env: process.env,
|
|
41
|
+
});
|
|
42
|
+
child.on("exit", (code) => resolve(code ?? 1));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const main = async () => {
|
|
46
|
+
const prepareExit = await run(path.join(packageRoot, "scripts", "prepare-stories.mjs"), []);
|
|
47
|
+
if (prepareExit !== 0) process.exit(prepareExit);
|
|
48
|
+
|
|
49
|
+
if (command === "dev") {
|
|
50
|
+
process.exit(await run(viteBin, ["--host", "127.0.0.1", "--port", "4100", "--strictPort"]));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (command === "build") {
|
|
54
|
+
process.exit(await run(viteBin, ["build"]));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (command === "preview") {
|
|
58
|
+
process.exit(
|
|
59
|
+
await run(viteBin, ["preview", "--host", "127.0.0.1", "--port", "4100", "--strictPort"]),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (command === "e2e" || command === "e2e:headed" || command === "e2e:ui") {
|
|
64
|
+
const playArgs = ["test", "-c", "playwright.config.ts"];
|
|
65
|
+
if (command === "e2e:headed") playArgs.push("--headed");
|
|
66
|
+
if (command === "e2e:ui") playArgs.push("--ui");
|
|
67
|
+
process.exit(await run(playwrightBin, playArgs));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.error(`Unknown command: ${command}`);
|
|
71
|
+
console.error("Usage: page-preview <dev|build|preview|e2e|e2e:headed|e2e:ui> [--stories <path>]");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
await main();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg: #09090b;--sidebar-bg: #111114;--surface: #18181b;--surface-hover: #27272a;--border: #27272a;--text: #fafafa;--text-muted: #a1a1aa;--primary: #6366f1;--primary-hover: #4f46e5;--primary-glow: rgba(99, 102, 241, .15);--radius: 12px;--transition-spring: cubic-bezier(.4, 0, .2, 1);--transition-bounce: cubic-bezier(.175, .885, .32, 1.275)}*{box-sizing:border-box}body{margin:0;font-family:Sora,Plus Jakarta Sans,Segoe UI,sans-serif;background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border);border-radius:10px}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}@keyframes pp-fade-in{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.pp-root{min-height:100vh;display:grid;grid-template-columns:260px 1fr;background:var(--bg)}.pp-sidebar{position:sticky;top:0;height:100vh;overflow:auto;border-right:1px solid var(--border);background:#111114b3;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);padding:16px;z-index:50}.pp-sidebar-head{margin-bottom:24px;padding-bottom:20px;border-bottom:1px solid var(--border)}.pp-sidebar-nav{display:grid;gap:24px}.pp-sidebar-group-title{margin:0 0 12px;font-size:10px;font-weight:700;letter-spacing:.15em;text-transform:uppercase;color:var(--text-muted)}.pp-sidebar-links{display:grid;gap:8px}.pp-sidebar-link{text-decoration:none;color:var(--text);font-size:13px;font-weight:500;border:1px solid transparent;border-radius:8px;background:transparent;padding:8px 12px;transition:all .2s var(--transition-spring);display:flex;align-items:center;gap:12px}.pp-sidebar-link:hover{background:var(--surface-hover);color:#fff}.pp-sidebar-link-active{border-color:var(--border);background:var(--surface-hover);color:var(--primary);font-weight:600;box-shadow:0 4px 12px #0003}.pp-sidebar-link-active:hover{background:var(--surface-hover)}.pp-main{min-width:0;display:flex;flex-direction:column}.pp-shell{max-width:1560px;width:100%;margin:0 auto;padding:24px}.pp-header{margin-bottom:24px}.pp-kicker{display:inline-block;padding:4px 10px;border:1px solid var(--border);border-radius:6px;background:#ffffff0d;color:var(--primary);font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.12em}.pp-title{margin:12px 0 8px;font-size:clamp(28px,3vw,40px);letter-spacing:-.02em}.pp-description{margin:0;color:var(--text-muted);font-size:14px}.pp-group{margin-top:24px}.pp-group-head{margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border)}.pp-group-title{margin:0;font-size:18px}.pp-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.pp-card{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);padding:12px;transition:all .3s var(--transition-spring);animation:pp-fade-in .6s var(--transition-spring) both}.pp-card:nth-child(2n){animation-delay:.1s}.pp-card:nth-child(3n){animation-delay:.2s}.pp-card:hover{border-color:#6366f166;box-shadow:0 12px 32px #00000080}.pp-frame-wrap{display:block;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:#000;transition:border-color .3s ease}.pp-card:hover .pp-frame-wrap{border-color:#6366f14d}.pp-card-title{margin:16px 0 8px;font-size:15px;font-weight:600;color:var(--text)}.pp-chip-row{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}.pp-chip{display:inline-flex;align-items:center;justify-content:center;text-decoration:none;color:var(--text-muted);border:1px solid var(--border);background:#ffffff08;border-radius:999px;min-height:28px;padding:4px 10px;font-size:11px;font-weight:500;transition:all .2s var(--transition-bounce)}.pp-chip:hover{background:var(--surface-hover);color:var(--text);border-color:var(--primary);box-shadow:0 4px 12px var(--primary-glow)}.pp-chip:active{background:var(--surface)}.pp-action-row{display:flex;justify-content:flex-end;margin-top:12px}.pp-open{display:inline-flex;align-items:center;justify-content:center;text-decoration:none;color:var(--text);border:1px solid var(--border);border-radius:8px;background:var(--surface-hover);min-height:36px;padding:0 16px;font-size:13px;font-weight:600;transition:all .25s var(--transition-bounce);gap:8px;cursor:pointer}.pp-open:hover{background:var(--primary);border-color:var(--primary);color:#fff;box-shadow:0 8px 20px var(--primary-glow)}.pp-open:active{background:var(--primary-hover)}.pp-viewer-root{min-width:0;display:flex;flex-direction:column}.pp-viewer-toolbar{position:sticky;top:0;z-index:20;border-bottom:1px solid var(--border);background:#09090bd9;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}.pp-viewer-top{max-width:1760px;margin:0 auto;width:100%;padding:12px 24px;display:flex;align-items:center;justify-content:space-between}.pp-viewer-back-wrap{display:flex;align-items:center}.pp-toolbar-back{width:36px;height:36px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:var(--text-muted);text-decoration:none;transition:all .2s var(--transition-bounce)}.pp-toolbar-back:hover{background:var(--surface-hover);color:var(--text);border-color:var(--primary);box-shadow:0 4px 12px var(--primary-glow)}.pp-toolbar-back:active{background:var(--surface-hover)}.pp-viewer-title{display:grid;gap:4px;text-align:center}.pp-viewer-title strong{font-size:11px;text-transform:uppercase;letter-spacing:.1em;color:var(--text-muted)}.pp-viewer-title span{font-size:18px;color:var(--text)}.pp-viewer-shell{flex:1;min-height:0}.pp-viewer-grid{padding:12px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;align-content:start}.pp-preview-card{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);padding:12px;transition:all .3s var(--transition-spring);animation:pp-fade-in .6s var(--transition-spring) both}.pp-preview-card:nth-child(2n){animation-delay:.1s}.pp-preview-card:nth-child(3n){animation-delay:.2s}.pp-preview-card:hover{border-color:#6366f166;box-shadow:0 8px 24px #0006}.pp-preview-card-active{border-color:var(--primary);box-shadow:0 0 0 1px var(--primary) inset,0 8px 32px var(--primary-glow)}.pp-canvas-shell{position:relative;width:100%;aspect-ratio:3 / 2;border:1px solid var(--border);border-radius:12px;background:#0f0f10;overflow:hidden}.pp-canvas-shell-compact{min-height:260px}.pp-canvas-inner{position:absolute;left:50%;top:50%;width:1920px;height:1280px;transform-origin:center center}.pp-viewer-frame{width:1920px;height:1280px;border:0;display:block}.pp-preview-card-footer{margin-top:8px;display:flex;align-items:center;justify-content:space-between;gap:8px}.pp-preview-card-title{margin:0;font-size:16px;color:var(--text)}.pp-preview-open-link{margin-left:auto;border-color:var(--primary);background:var(--primary);color:#fff!important;box-shadow:0 4px 12px var(--primary-glow)}.pp-preview-open-link:hover{background:var(--primary-hover);border-color:var(--primary-hover);box-shadow:0 8px 24px var(--primary-glow)}.pp-preview-open-link:active{background:var(--primary-hover)}@media(min-width:1700px){.pp-grid,.pp-viewer-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(max-width:1180px){.pp-grid,.pp-viewer-grid{grid-template-columns:1fr}}@media(max-width:900px){.pp-root{grid-template-columns:1fr}.pp-sidebar{position:static;height:auto;border-right:0;border-bottom:1px solid var(--border)}.pp-shell{padding:16px 12px 24px}.pp-viewer-top{padding:12px}.pp-viewer-title{text-align:left}}
|