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,100 @@
1
+ import { decodePreviewState } from "./state-codec";
2
+ import type { PreviewStateSnapshot } from "./types";
3
+
4
+ type ZustandStoreLike = {
5
+ setState: (state: Record<string, unknown>) => unknown;
6
+ };
7
+
8
+ type ReduxStoreLike = {
9
+ dispatch: (action: unknown) => unknown;
10
+ };
11
+
12
+ type ContextSetter = (value: unknown) => void;
13
+
14
+ type ReactQueryClientLike = {
15
+ setQueryData: (queryKey: unknown[], data: unknown) => unknown;
16
+ };
17
+
18
+ export const createPreviewBridge = (options?: {
19
+ queryKey?: string;
20
+ developmentOnly?: boolean;
21
+ }) => {
22
+ const queryKey = options?.queryKey ?? "__pp";
23
+ const developmentOnly = options?.developmentOnly ?? true;
24
+
25
+ const zustandStores = new Map<string, ZustandStoreLike>();
26
+ const reduxStores = new Map<string, ReduxStoreLike>();
27
+ const contextSetters = new Map<string, ContextSetter>();
28
+ const queryClients = new Map<string, ReactQueryClientLike>();
29
+
30
+ const registerZustandStore = (storeId: string, store: ZustandStoreLike) => {
31
+ zustandStores.set(storeId, store);
32
+ };
33
+
34
+ const registerReduxStore = (storeId: string, store: ReduxStoreLike) => {
35
+ reduxStores.set(storeId, store);
36
+ };
37
+
38
+ const registerContextSetter = (contextId: string, setter: ContextSetter) => {
39
+ contextSetters.set(contextId, setter);
40
+ };
41
+
42
+ const registerReactQueryClient = (clientId: string, client: ReactQueryClientLike) => {
43
+ queryClients.set(clientId, client);
44
+ };
45
+
46
+ const applySnapshot = (snapshot: PreviewStateSnapshot) => {
47
+ snapshot.zustand?.forEach(({ storeId, state }) => {
48
+ const store = zustandStores.get(storeId);
49
+ if (!store) return;
50
+ store.setState(state);
51
+ });
52
+
53
+ snapshot.redux?.forEach(({ storeId, action }) => {
54
+ const store = reduxStores.get(storeId);
55
+ if (!store) return;
56
+ store.dispatch(action);
57
+ });
58
+
59
+ snapshot.context?.forEach(({ contextId, value }) => {
60
+ const setter = contextSetters.get(contextId);
61
+ if (!setter) return;
62
+ setter(value);
63
+ });
64
+
65
+ snapshot.reactQuery?.forEach(({ queryKey, data }) => {
66
+ queryClients.forEach((client) => {
67
+ client.setQueryData(queryKey, data);
68
+ });
69
+ });
70
+ };
71
+
72
+ const applyFromSearch = (search: string) => {
73
+ if (developmentOnly && process.env.NODE_ENV !== "development") return;
74
+ const params = new URLSearchParams(search);
75
+ const encoded = params.get(queryKey);
76
+ if (!encoded) return;
77
+
78
+ const snapshot = decodePreviewState(encoded);
79
+ if (!snapshot) return;
80
+
81
+ applySnapshot(snapshot);
82
+ };
83
+
84
+ const reset = () => {
85
+ zustandStores.clear();
86
+ reduxStores.clear();
87
+ contextSetters.clear();
88
+ queryClients.clear();
89
+ };
90
+
91
+ return {
92
+ registerZustandStore,
93
+ registerReduxStore,
94
+ registerContextSetter,
95
+ registerReactQueryClient,
96
+ applySnapshot,
97
+ applyFromSearch,
98
+ reset,
99
+ };
100
+ };
@@ -0,0 +1,26 @@
1
+ import type { PreviewStateSnapshot } from "./types";
2
+
3
+ const toBase64Url = (value: string) =>
4
+ btoa(unescape(encodeURIComponent(value)))
5
+ .replace(/\+/g, "-")
6
+ .replace(/\//g, "_")
7
+ .replace(/=+$/g, "");
8
+
9
+ const fromBase64Url = (value: string) => {
10
+ const padded = value + "===".slice((value.length + 3) % 4);
11
+ const base64 = padded.replace(/-/g, "+").replace(/_/g, "/");
12
+ return decodeURIComponent(escape(atob(base64)));
13
+ };
14
+
15
+ export const encodePreviewState = (snapshot: PreviewStateSnapshot) =>
16
+ toBase64Url(JSON.stringify(snapshot));
17
+
18
+ export const decodePreviewState = (encoded: string): PreviewStateSnapshot | null => {
19
+ try {
20
+ const parsed = JSON.parse(fromBase64Url(encoded));
21
+ if (!parsed || typeof parsed !== "object") return null;
22
+ return parsed as PreviewStateSnapshot;
23
+ } catch {
24
+ return null;
25
+ }
26
+ };
@@ -0,0 +1,49 @@
1
+ export type PagePreviewVariant = {
2
+ id: string;
3
+ label?: string;
4
+ state?: PreviewStateSnapshot;
5
+ };
6
+
7
+ export type PagePreviewTarget = {
8
+ path: string;
9
+ origin?: string;
10
+ variantQueryKey?: string;
11
+ stateQueryKey?: string;
12
+ };
13
+
14
+ export type PagePreviewEntry = {
15
+ id: string;
16
+ group?: string;
17
+ name?: string;
18
+ title?: string;
19
+ description?: string;
20
+ target: PagePreviewTarget;
21
+ variants: PagePreviewVariant[];
22
+ };
23
+
24
+ export type ZustandInjection = {
25
+ storeId: string;
26
+ state: Record<string, unknown>;
27
+ };
28
+
29
+ export type ReduxInjection = {
30
+ storeId: string;
31
+ action: unknown;
32
+ };
33
+
34
+ export type ContextInjection = {
35
+ contextId: string;
36
+ value: unknown;
37
+ };
38
+
39
+ export type ReactQueryInjection = {
40
+ queryKey: unknown[];
41
+ data: unknown;
42
+ };
43
+
44
+ export type PreviewStateSnapshot = {
45
+ zustand?: ZustandInjection[];
46
+ redux?: ReduxInjection[];
47
+ context?: ContextInjection[];
48
+ reactQuery?: ReactQueryInjection[];
49
+ };
package/src/main.tsx ADDED
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { BrowserRouter } from "react-router-dom";
4
+
5
+ import App from "./App";
6
+ import "./styles/app.css";
7
+
8
+ createRoot(document.getElementById("root")!).render(
9
+ <React.StrictMode>
10
+ <BrowserRouter>
11
+ <App />
12
+ </BrowserRouter>
13
+ </React.StrictMode>,
14
+ );
@@ -0,0 +1,3 @@
1
+ import type { PagePreviewEntry } from "../lib/types";
2
+
3
+ export const pagePreviewStories: PagePreviewEntry[] = [];
@@ -0,0 +1,4 @@
1
+ import type { PagePreviewEntry } from "../lib/types";
2
+ import { pagePreviewStories as injectedStories } from "./injected";
3
+
4
+ export const pagePreviewStories: PagePreviewEntry[] = injectedStories;
@@ -0,0 +1 @@
1
+ export { pagePreviewStories } from "./default";
@@ -0,0 +1,480 @@
1
+ :root {
2
+ --bg: #09090b;
3
+ --sidebar-bg: #111114;
4
+ --surface: #18181b;
5
+ --surface-hover: #27272a;
6
+ --border: #27272a;
7
+ --text: #fafafa;
8
+ --text-muted: #a1a1aa;
9
+ --primary: #6366f1;
10
+ --primary-hover: #4f46e5;
11
+ --primary-glow: rgba(99, 102, 241, 0.15);
12
+ --radius: 12px;
13
+ --transition-spring: cubic-bezier(0.4, 0, 0.2, 1);
14
+ --transition-bounce: cubic-bezier(0.175, 0.885, 0.32, 1.275);
15
+ }
16
+
17
+ * { box-sizing: border-box; }
18
+
19
+ body {
20
+ margin: 0;
21
+ font-family: "Sora", "Plus Jakarta Sans", "Segoe UI", sans-serif;
22
+ background: var(--bg);
23
+ color: var(--text);
24
+ -webkit-font-smoothing: antialiased;
25
+ }
26
+
27
+ /* Custom Scrollbar */
28
+ ::-webkit-scrollbar {
29
+ width: 6px;
30
+ height: 6px;
31
+ }
32
+ ::-webkit-scrollbar-track {
33
+ background: transparent;
34
+ }
35
+ ::-webkit-scrollbar-thumb {
36
+ background: var(--border);
37
+ border-radius: 10px;
38
+ }
39
+ ::-webkit-scrollbar-thumb:hover {
40
+ background: var(--text-muted);
41
+ }
42
+
43
+ @keyframes pp-fade-in {
44
+ from { opacity: 0; transform: translateY(10px); }
45
+ to { opacity: 1; transform: translateY(0); }
46
+ }
47
+
48
+ .pp-root {
49
+ min-height: 100vh;
50
+ display: grid;
51
+ grid-template-columns: 260px 1fr;
52
+ background: var(--bg);
53
+ }
54
+
55
+ .pp-sidebar {
56
+ position: sticky;
57
+ top: 0;
58
+ height: 100vh;
59
+ overflow: auto;
60
+ border-right: 1px solid var(--border);
61
+ background: rgba(17, 17, 20, 0.7);
62
+ backdrop-filter: blur(20px);
63
+ padding: 16px;
64
+ z-index: 50;
65
+ }
66
+
67
+ .pp-sidebar-head {
68
+ margin-bottom: 24px;
69
+ padding-bottom: 20px;
70
+ border-bottom: 1px solid var(--border);
71
+ }
72
+
73
+ .pp-sidebar-nav {
74
+ display: grid;
75
+ gap: 24px;
76
+ }
77
+
78
+ .pp-sidebar-group-title {
79
+ margin: 0 0 12px;
80
+ font-size: 10px;
81
+ font-weight: 700;
82
+ letter-spacing: 0.15em;
83
+ text-transform: uppercase;
84
+ color: var(--text-muted);
85
+ }
86
+
87
+ .pp-sidebar-links {
88
+ display: grid;
89
+ gap: 8px;
90
+ }
91
+
92
+ .pp-sidebar-link {
93
+ text-decoration: none;
94
+ color: var(--text);
95
+ font-size: 13px;
96
+ font-weight: 500;
97
+ border: 1px solid transparent;
98
+ border-radius: 8px;
99
+ background: transparent;
100
+ padding: 8px 12px;
101
+ transition: all 0.2s var(--transition-spring);
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 12px;
105
+ }
106
+
107
+ .pp-sidebar-link:hover {
108
+ background: var(--surface-hover);
109
+ color: #fff;
110
+ }
111
+
112
+ .pp-sidebar-link-active {
113
+ border-color: var(--border);
114
+ background: var(--surface-hover);
115
+ color: var(--primary);
116
+ font-weight: 600;
117
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
118
+ }
119
+
120
+ .pp-sidebar-link-active:hover {
121
+ background: var(--surface-hover);
122
+ }
123
+
124
+ .pp-main {
125
+ min-width: 0;
126
+ display: flex;
127
+ flex-direction: column;
128
+ }
129
+
130
+ .pp-shell {
131
+ max-width: 1560px;
132
+ width: 100%;
133
+ margin: 0 auto;
134
+ padding: 24px;
135
+ }
136
+
137
+ .pp-header { margin-bottom: 24px; }
138
+
139
+ .pp-kicker {
140
+ display: inline-block;
141
+ padding: 4px 10px;
142
+ border: 1px solid var(--border);
143
+ border-radius: 6px;
144
+ background: rgba(255, 255, 255, 0.05);
145
+ color: var(--primary);
146
+ font-size: 10px;
147
+ font-weight: 700;
148
+ text-transform: uppercase;
149
+ letter-spacing: 0.12em;
150
+ }
151
+
152
+ .pp-title {
153
+ margin: 12px 0 8px;
154
+ font-size: clamp(28px, 3vw, 40px);
155
+ letter-spacing: -0.02em;
156
+ }
157
+
158
+ .pp-description {
159
+ margin: 0;
160
+ color: var(--text-muted);
161
+ font-size: 14px;
162
+ }
163
+
164
+ .pp-group { margin-top: 24px; }
165
+
166
+ .pp-group-head {
167
+ margin-bottom: 12px;
168
+ padding-bottom: 8px;
169
+ border-bottom: 1px solid var(--border);
170
+ }
171
+
172
+ .pp-group-title {
173
+ margin: 0;
174
+ font-size: 18px;
175
+ }
176
+
177
+ .pp-grid {
178
+ display: grid;
179
+ grid-template-columns: repeat(2, minmax(0, 1fr));
180
+ gap: 12px;
181
+ }
182
+
183
+ .pp-card {
184
+ border: 1px solid var(--border);
185
+ border-radius: var(--radius);
186
+ background: var(--surface);
187
+ padding: 12px;
188
+ transition: all 0.3s var(--transition-spring);
189
+ animation: pp-fade-in 0.6s var(--transition-spring) both;
190
+ }
191
+
192
+ .pp-card:nth-child(2n) { animation-delay: 0.1s; }
193
+ .pp-card:nth-child(3n) { animation-delay: 0.2s; }
194
+
195
+ .pp-card:hover {
196
+ border-color: rgba(99, 102, 241, 0.4);
197
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
198
+ }
199
+
200
+ .pp-frame-wrap {
201
+ display: block;
202
+ border: 1px solid var(--border);
203
+ border-radius: 8px;
204
+ overflow: hidden;
205
+ background: #000;
206
+ transition: border-color 0.3s ease;
207
+ }
208
+
209
+ .pp-card:hover .pp-frame-wrap {
210
+ border-color: rgba(99, 102, 241, 0.3);
211
+ }
212
+
213
+ .pp-card-title {
214
+ margin: 16px 0 8px;
215
+ font-size: 15px;
216
+ font-weight: 600;
217
+ color: var(--text);
218
+ }
219
+
220
+ .pp-chip-row {
221
+ display: flex;
222
+ flex-wrap: wrap;
223
+ gap: 8px;
224
+ margin-bottom: 12px;
225
+ }
226
+
227
+ .pp-chip {
228
+ display: inline-flex;
229
+ align-items: center;
230
+ justify-content: center;
231
+ text-decoration: none;
232
+ color: var(--text-muted);
233
+ border: 1px solid var(--border);
234
+ background: rgba(255, 255, 255, 0.03);
235
+ border-radius: 999px;
236
+ min-height: 28px;
237
+ padding: 4px 10px;
238
+ font-size: 11px;
239
+ font-weight: 500;
240
+ transition: all 0.2s var(--transition-bounce);
241
+ }
242
+
243
+ .pp-chip:hover {
244
+ background: var(--surface-hover);
245
+ color: var(--text);
246
+ border-color: var(--primary);
247
+ box-shadow: 0 4px 12px var(--primary-glow);
248
+ }
249
+
250
+ .pp-chip:active {
251
+ background: var(--surface);
252
+ }
253
+
254
+ .pp-action-row {
255
+ display: flex;
256
+ justify-content: flex-end;
257
+ margin-top: 12px;
258
+ }
259
+
260
+ .pp-open {
261
+ display: inline-flex;
262
+ align-items: center;
263
+ justify-content: center;
264
+ text-decoration: none;
265
+ color: var(--text);
266
+ border: 1px solid var(--border);
267
+ border-radius: 8px;
268
+ background: var(--surface-hover);
269
+ min-height: 36px;
270
+ padding: 0 16px;
271
+ font-size: 13px;
272
+ font-weight: 600;
273
+ transition: all 0.25s var(--transition-bounce);
274
+ gap: 8px;
275
+ cursor: pointer;
276
+ }
277
+
278
+ .pp-open:hover {
279
+ background: var(--primary);
280
+ border-color: var(--primary);
281
+ color: #fff;
282
+ box-shadow: 0 8px 20px var(--primary-glow);
283
+ }
284
+
285
+ .pp-open:active {
286
+ background: var(--primary-hover);
287
+ }
288
+
289
+ .pp-viewer-root {
290
+ min-width: 0;
291
+ display: flex;
292
+ flex-direction: column;
293
+ }
294
+
295
+ .pp-viewer-toolbar {
296
+ position: sticky;
297
+ top: 0;
298
+ z-index: 20;
299
+ border-bottom: 1px solid var(--border);
300
+ background: rgba(9, 9, 11, 0.85);
301
+ backdrop-filter: blur(12px);
302
+ }
303
+
304
+ .pp-viewer-top {
305
+ max-width: 1760px;
306
+ margin: 0 auto;
307
+ width: 100%;
308
+ padding: 12px 24px;
309
+ display: flex;
310
+ align-items: center;
311
+ justify-content: space-between;
312
+ }
313
+
314
+ .pp-viewer-back-wrap {
315
+ display: flex;
316
+ align-items: center;
317
+ }
318
+
319
+ .pp-toolbar-back {
320
+ width: 36px;
321
+ height: 36px;
322
+ display: flex;
323
+ align-items: center;
324
+ justify-content: center;
325
+ border: 1px solid var(--border);
326
+ border-radius: 8px;
327
+ background: var(--surface);
328
+ color: var(--text-muted);
329
+ text-decoration: none;
330
+ transition: all 0.2s var(--transition-bounce);
331
+ }
332
+
333
+ .pp-toolbar-back:hover {
334
+ background: var(--surface-hover);
335
+ color: var(--text);
336
+ border-color: var(--primary);
337
+ box-shadow: 0 4px 12px var(--primary-glow);
338
+ }
339
+
340
+ .pp-toolbar-back:active {
341
+ background: var(--surface-hover);
342
+ }
343
+
344
+ .pp-viewer-title {
345
+ display: grid;
346
+ gap: 4px;
347
+ text-align: center;
348
+ }
349
+
350
+ .pp-viewer-title strong {
351
+ font-size: 11px;
352
+ text-transform: uppercase;
353
+ letter-spacing: 0.1em;
354
+ color: var(--text-muted);
355
+ }
356
+
357
+ .pp-viewer-title span {
358
+ font-size: 18px;
359
+ color: var(--text);
360
+ }
361
+
362
+ .pp-viewer-shell {
363
+ flex: 1;
364
+ min-height: 0;
365
+ }
366
+
367
+ .pp-viewer-grid {
368
+ padding: 12px;
369
+ display: grid;
370
+ grid-template-columns: repeat(2, minmax(0, 1fr));
371
+ gap: 12px;
372
+ align-content: start;
373
+ }
374
+
375
+ .pp-preview-card {
376
+ border: 1px solid var(--border);
377
+ border-radius: var(--radius);
378
+ background: var(--surface);
379
+ padding: 12px;
380
+ transition: all 0.3s var(--transition-spring);
381
+ animation: pp-fade-in 0.6s var(--transition-spring) both;
382
+ }
383
+
384
+ .pp-preview-card:nth-child(2n) { animation-delay: 0.1s; }
385
+ .pp-preview-card:nth-child(3n) { animation-delay: 0.2s; }
386
+
387
+ .pp-preview-card:hover {
388
+ border-color: rgba(99, 102, 241, 0.4);
389
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
390
+ }
391
+
392
+ .pp-preview-card-active {
393
+ border-color: var(--primary);
394
+ box-shadow: 0 0 0 1px var(--primary) inset, 0 8px 32px var(--primary-glow);
395
+ }
396
+
397
+ .pp-canvas-shell {
398
+ position: relative;
399
+ width: 100%;
400
+ aspect-ratio: 3 / 2;
401
+ border: 1px solid var(--border);
402
+ border-radius: 12px;
403
+ background: #0f0f10;
404
+ overflow: hidden;
405
+ }
406
+
407
+ .pp-canvas-shell-compact {
408
+ min-height: 260px;
409
+ }
410
+
411
+ .pp-canvas-inner {
412
+ position: absolute;
413
+ left: 50%;
414
+ top: 50%;
415
+ width: 1920px;
416
+ height: 1280px;
417
+ transform-origin: center center;
418
+ }
419
+
420
+ .pp-viewer-frame {
421
+ width: 1920px;
422
+ height: 1280px;
423
+ border: 0;
424
+ display: block;
425
+ }
426
+
427
+ .pp-preview-card-footer {
428
+ margin-top: 8px;
429
+ display: flex;
430
+ align-items: center;
431
+ justify-content: space-between;
432
+ gap: 8px;
433
+ }
434
+
435
+ .pp-preview-card-title {
436
+ margin: 0;
437
+ font-size: 16px;
438
+ color: var(--text);
439
+ }
440
+
441
+ .pp-preview-open-link {
442
+ margin-left: auto;
443
+ border-color: var(--primary);
444
+ background: var(--primary);
445
+ color: #fff !important;
446
+ box-shadow: 0 4px 12px var(--primary-glow);
447
+ }
448
+
449
+ .pp-preview-open-link:hover {
450
+ background: var(--primary-hover);
451
+ border-color: var(--primary-hover);
452
+ box-shadow: 0 8px 24px var(--primary-glow);
453
+ }
454
+
455
+ .pp-preview-open-link:active {
456
+ background: var(--primary-hover);
457
+ }
458
+
459
+ @media (min-width: 1700px) {
460
+ .pp-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
461
+ .pp-viewer-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
462
+ }
463
+
464
+ @media (max-width: 1180px) {
465
+ .pp-grid { grid-template-columns: 1fr; }
466
+ .pp-viewer-grid { grid-template-columns: 1fr; }
467
+ }
468
+
469
+ @media (max-width: 900px) {
470
+ .pp-root { grid-template-columns: 1fr; }
471
+ .pp-sidebar {
472
+ position: static;
473
+ height: auto;
474
+ border-right: 0;
475
+ border-bottom: 1px solid var(--border);
476
+ }
477
+ .pp-shell { padding: 16px 12px 24px; }
478
+ .pp-viewer-top { padding: 12px; }
479
+ .pp-viewer-title { text-align: left; }
480
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ });