@epiccontext/mcp 0.1.41 → 0.1.43
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/dist/cli/commands/add-storybook.d.ts +5 -0
- package/dist/cli/commands/add-storybook.d.ts.map +1 -0
- package/dist/cli/commands/add-storybook.js +51 -0
- package/dist/cli/commands/add-storybook.js.map +1 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +10 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +15 -0
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/files.d.ts +13 -0
- package/dist/cli/files.d.ts.map +1 -1
- package/dist/cli/files.js +68 -0
- package/dist/cli/files.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/templates/storybook.d.ts +16 -0
- package/dist/templates/storybook.d.ts.map +1 -0
- package/dist/templates/storybook.js +991 -0
- package/dist/templates/storybook.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storybook Template
|
|
3
|
+
*
|
|
4
|
+
* This file contains the complete Storybook template with EpicContext authentication middleware.
|
|
5
|
+
* The template is bundled as string constants to ensure it's included in the npm package.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Get all Storybook template files
|
|
9
|
+
* Files are organized to be written to CONTEXT/design-system/storybook/
|
|
10
|
+
*/
|
|
11
|
+
export function getStorybookTemplateFiles() {
|
|
12
|
+
return [
|
|
13
|
+
// Package configuration
|
|
14
|
+
{
|
|
15
|
+
path: "package.json",
|
|
16
|
+
content: `{
|
|
17
|
+
"name": "@epiccontext/storybook-template",
|
|
18
|
+
"version": "1.0.0",
|
|
19
|
+
"private": true,
|
|
20
|
+
"description": "EpicContext Storybook template with authentication middleware",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "storybook dev -p 6006",
|
|
23
|
+
"build": "storybook build && node scripts/postbuild.mjs",
|
|
24
|
+
"preview": "npx serve storybook-static"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"class-variance-authority": "^0.7.0",
|
|
28
|
+
"clsx": "^2.1.1",
|
|
29
|
+
"lucide-react": "^0.460.0",
|
|
30
|
+
"react": "^18.2.0",
|
|
31
|
+
"react-dom": "^18.2.0",
|
|
32
|
+
"tailwind-merge": "^2.5.2",
|
|
33
|
+
"tailwindcss-animate": "^1.0.7"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@storybook/addon-essentials": "^8.4.0",
|
|
37
|
+
"@storybook/addon-interactions": "^8.4.0",
|
|
38
|
+
"@storybook/addon-links": "^8.4.0",
|
|
39
|
+
"@storybook/blocks": "^8.4.0",
|
|
40
|
+
"@storybook/react": "^8.4.0",
|
|
41
|
+
"@storybook/react-vite": "^8.4.0",
|
|
42
|
+
"@storybook/test": "^8.4.0",
|
|
43
|
+
"@types/node": "^20.0.0",
|
|
44
|
+
"@types/react": "^18.2.0",
|
|
45
|
+
"@types/react-dom": "^18.2.0",
|
|
46
|
+
"autoprefixer": "^10.4.20",
|
|
47
|
+
"postcss": "^8.4.47",
|
|
48
|
+
"storybook": "^8.4.0",
|
|
49
|
+
"tailwindcss": "^3.4.14",
|
|
50
|
+
"typescript": "^5.0.0",
|
|
51
|
+
"vite": "^5.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`,
|
|
55
|
+
},
|
|
56
|
+
// TypeScript configuration
|
|
57
|
+
{
|
|
58
|
+
path: "tsconfig.json",
|
|
59
|
+
content: `{
|
|
60
|
+
"compilerOptions": {
|
|
61
|
+
"target": "ES2020",
|
|
62
|
+
"useDefineForClassFields": true,
|
|
63
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
64
|
+
"module": "ESNext",
|
|
65
|
+
"skipLibCheck": true,
|
|
66
|
+
"moduleResolution": "bundler",
|
|
67
|
+
"allowImportingTsExtensions": true,
|
|
68
|
+
"resolveJsonModule": true,
|
|
69
|
+
"isolatedModules": true,
|
|
70
|
+
"noEmit": true,
|
|
71
|
+
"jsx": "react-jsx",
|
|
72
|
+
"strict": true,
|
|
73
|
+
"noUnusedLocals": true,
|
|
74
|
+
"noUnusedParameters": true,
|
|
75
|
+
"noFallthroughCasesInSwitch": true,
|
|
76
|
+
"baseUrl": ".",
|
|
77
|
+
"paths": {
|
|
78
|
+
"@/*": ["./*"]
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
82
|
+
"exclude": ["node_modules"]
|
|
83
|
+
}
|
|
84
|
+
`,
|
|
85
|
+
},
|
|
86
|
+
// Tailwind configuration
|
|
87
|
+
{
|
|
88
|
+
path: "tailwind.config.ts",
|
|
89
|
+
content: `import type { Config } from "tailwindcss";
|
|
90
|
+
|
|
91
|
+
const config: Config = {
|
|
92
|
+
darkMode: ["class"],
|
|
93
|
+
content: [
|
|
94
|
+
"./stories/**/*.{js,ts,jsx,tsx,mdx}",
|
|
95
|
+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
96
|
+
],
|
|
97
|
+
theme: {
|
|
98
|
+
extend: {
|
|
99
|
+
colors: {
|
|
100
|
+
background: "hsl(var(--background))",
|
|
101
|
+
foreground: "hsl(var(--foreground))",
|
|
102
|
+
card: {
|
|
103
|
+
DEFAULT: "hsl(var(--card))",
|
|
104
|
+
foreground: "hsl(var(--card-foreground))",
|
|
105
|
+
},
|
|
106
|
+
popover: {
|
|
107
|
+
DEFAULT: "hsl(var(--popover))",
|
|
108
|
+
foreground: "hsl(var(--popover-foreground))",
|
|
109
|
+
},
|
|
110
|
+
primary: {
|
|
111
|
+
DEFAULT: "hsl(var(--primary))",
|
|
112
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
113
|
+
},
|
|
114
|
+
secondary: {
|
|
115
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
116
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
117
|
+
},
|
|
118
|
+
muted: {
|
|
119
|
+
DEFAULT: "hsl(var(--muted))",
|
|
120
|
+
foreground: "hsl(var(--muted-foreground))",
|
|
121
|
+
},
|
|
122
|
+
accent: {
|
|
123
|
+
DEFAULT: "hsl(var(--accent))",
|
|
124
|
+
foreground: "hsl(var(--accent-foreground))",
|
|
125
|
+
},
|
|
126
|
+
destructive: {
|
|
127
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
128
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
129
|
+
},
|
|
130
|
+
border: "hsl(var(--border))",
|
|
131
|
+
input: "hsl(var(--input))",
|
|
132
|
+
ring: "hsl(var(--ring))",
|
|
133
|
+
chart: {
|
|
134
|
+
"1": "hsl(var(--chart-1))",
|
|
135
|
+
"2": "hsl(var(--chart-2))",
|
|
136
|
+
"3": "hsl(var(--chart-3))",
|
|
137
|
+
"4": "hsl(var(--chart-4))",
|
|
138
|
+
"5": "hsl(var(--chart-5))",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
borderRadius: {
|
|
142
|
+
lg: "var(--radius)",
|
|
143
|
+
md: "calc(var(--radius) - 2px)",
|
|
144
|
+
sm: "calc(var(--radius) - 4px)",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
plugins: [require("tailwindcss-animate")],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export default config;
|
|
152
|
+
`,
|
|
153
|
+
},
|
|
154
|
+
// PostCSS configuration
|
|
155
|
+
{
|
|
156
|
+
path: "postcss.config.js",
|
|
157
|
+
content: `module.exports = {
|
|
158
|
+
plugins: {
|
|
159
|
+
tailwindcss: {},
|
|
160
|
+
autoprefixer: {},
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
`,
|
|
164
|
+
},
|
|
165
|
+
// Vercel configuration
|
|
166
|
+
{
|
|
167
|
+
path: "vercel.json",
|
|
168
|
+
content: `{
|
|
169
|
+
"$schema": "https://openapi.vercel.sh/vercel.json",
|
|
170
|
+
"buildCommand": "npm run build",
|
|
171
|
+
"installCommand": "npm install",
|
|
172
|
+
"framework": null
|
|
173
|
+
}
|
|
174
|
+
`,
|
|
175
|
+
},
|
|
176
|
+
// Git ignore
|
|
177
|
+
{
|
|
178
|
+
path: ".gitignore",
|
|
179
|
+
content: `# Dependencies
|
|
180
|
+
node_modules/
|
|
181
|
+
|
|
182
|
+
# Build output
|
|
183
|
+
storybook-static/
|
|
184
|
+
dist/
|
|
185
|
+
.vercel/
|
|
186
|
+
|
|
187
|
+
# Environment
|
|
188
|
+
.env
|
|
189
|
+
.env.local
|
|
190
|
+
.env.*.local
|
|
191
|
+
|
|
192
|
+
# IDE
|
|
193
|
+
.vscode/
|
|
194
|
+
.idea/
|
|
195
|
+
*.swp
|
|
196
|
+
*.swo
|
|
197
|
+
|
|
198
|
+
# OS
|
|
199
|
+
.DS_Store
|
|
200
|
+
Thumbs.db
|
|
201
|
+
|
|
202
|
+
# Logs
|
|
203
|
+
*.log
|
|
204
|
+
npm-debug.log*
|
|
205
|
+
|
|
206
|
+
# Cache
|
|
207
|
+
.cache/
|
|
208
|
+
`,
|
|
209
|
+
},
|
|
210
|
+
// README
|
|
211
|
+
{
|
|
212
|
+
path: "README.md",
|
|
213
|
+
content: `# Design System Storybook
|
|
214
|
+
|
|
215
|
+
This is your project's design system, powered by Storybook with EpicContext authentication.
|
|
216
|
+
|
|
217
|
+
## Quick Start
|
|
218
|
+
|
|
219
|
+
\`\`\`bash
|
|
220
|
+
# Install dependencies
|
|
221
|
+
npm install
|
|
222
|
+
|
|
223
|
+
# Start Storybook (development)
|
|
224
|
+
npm run dev
|
|
225
|
+
|
|
226
|
+
# Build for production
|
|
227
|
+
npm run build
|
|
228
|
+
\`\`\`
|
|
229
|
+
|
|
230
|
+
## Adding Components
|
|
231
|
+
|
|
232
|
+
1. Create a \`components/ui/\` folder
|
|
233
|
+
2. Add your component (e.g., \`Button.tsx\`)
|
|
234
|
+
3. Add a story file (e.g., \`Button.stories.tsx\`)
|
|
235
|
+
4. Components will appear in Storybook automatically
|
|
236
|
+
|
|
237
|
+
## Deploying to Vercel
|
|
238
|
+
|
|
239
|
+
1. Push this folder to a GitHub repository
|
|
240
|
+
2. Connect it to Vercel
|
|
241
|
+
3. Add environment variables:
|
|
242
|
+
- \`EPICCONTEXT_STORYBOOK_SECRET\` - Get from EpicContext settings
|
|
243
|
+
- \`EPICCONTEXT_APP_URL\` - Your EpicContext instance URL
|
|
244
|
+
4. Deploy!
|
|
245
|
+
|
|
246
|
+
## Authentication
|
|
247
|
+
|
|
248
|
+
When deployed with the environment variables set, your Storybook is protected.
|
|
249
|
+
Only team members logged into EpicContext can access it.
|
|
250
|
+
|
|
251
|
+
During local development, authentication is bypassed.
|
|
252
|
+
|
|
253
|
+
## Customization
|
|
254
|
+
|
|
255
|
+
- **Design tokens**: Edit \`styles/globals.css\`
|
|
256
|
+
- **Tailwind config**: Edit \`tailwind.config.ts\`
|
|
257
|
+
- **Storybook config**: Edit \`.storybook/main.ts\` and \`.storybook/preview.ts\`
|
|
258
|
+
|
|
259
|
+
## Structure
|
|
260
|
+
|
|
261
|
+
\`\`\`
|
|
262
|
+
storybook/
|
|
263
|
+
├── .storybook/ # Storybook configuration
|
|
264
|
+
├── components/ # Your components (create this)
|
|
265
|
+
├── lib/ # Utilities
|
|
266
|
+
├── stories/ # Documentation pages
|
|
267
|
+
├── styles/ # CSS and design tokens
|
|
268
|
+
├── scripts/ # Build scripts
|
|
269
|
+
└── middleware.js # EpicContext auth
|
|
270
|
+
\`\`\`
|
|
271
|
+
`,
|
|
272
|
+
},
|
|
273
|
+
// Storybook main config
|
|
274
|
+
{
|
|
275
|
+
path: ".storybook/main.ts",
|
|
276
|
+
content: `import type { StorybookConfig } from "@storybook/react-vite";
|
|
277
|
+
|
|
278
|
+
const config: StorybookConfig = {
|
|
279
|
+
stories: [
|
|
280
|
+
"../stories/**/*.mdx",
|
|
281
|
+
"../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
|
|
282
|
+
"../components/**/*.stories.@(js|jsx|mjs|ts|tsx)",
|
|
283
|
+
],
|
|
284
|
+
addons: [
|
|
285
|
+
"@storybook/addon-links",
|
|
286
|
+
"@storybook/addon-essentials",
|
|
287
|
+
"@storybook/addon-interactions",
|
|
288
|
+
],
|
|
289
|
+
framework: {
|
|
290
|
+
name: "@storybook/react-vite",
|
|
291
|
+
options: {},
|
|
292
|
+
},
|
|
293
|
+
docs: {
|
|
294
|
+
autodocs: "tag",
|
|
295
|
+
},
|
|
296
|
+
staticDirs: ["../public"],
|
|
297
|
+
viteFinal: async (config) => {
|
|
298
|
+
// Add path aliases to match component imports
|
|
299
|
+
config.resolve = config.resolve || {};
|
|
300
|
+
config.resolve.alias = {
|
|
301
|
+
...config.resolve.alias,
|
|
302
|
+
// Local paths - create components folder and add components here
|
|
303
|
+
"@/components": new URL("../components", import.meta.url).pathname,
|
|
304
|
+
"@/lib": new URL("../lib", import.meta.url).pathname,
|
|
305
|
+
// Next.js mocks for Storybook (allows using Next.js components)
|
|
306
|
+
"next/navigation": new URL("../lib/next-mocks.ts", import.meta.url).pathname,
|
|
307
|
+
"next/link": new URL("../lib/next-mocks.ts", import.meta.url).pathname,
|
|
308
|
+
"next/dynamic": new URL("../lib/next-mocks.ts", import.meta.url).pathname,
|
|
309
|
+
};
|
|
310
|
+
return config;
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
export default config;
|
|
315
|
+
`,
|
|
316
|
+
},
|
|
317
|
+
// Storybook preview config
|
|
318
|
+
{
|
|
319
|
+
path: ".storybook/preview.ts",
|
|
320
|
+
content: `import type { Preview } from "@storybook/react";
|
|
321
|
+
import "../styles/globals.css";
|
|
322
|
+
|
|
323
|
+
const preview: Preview = {
|
|
324
|
+
parameters: {
|
|
325
|
+
controls: {
|
|
326
|
+
matchers: {
|
|
327
|
+
color: /(background|color)$/i,
|
|
328
|
+
date: /Date$/i,
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
docs: {
|
|
332
|
+
toc: true,
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
decorators: [],
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
export default preview;
|
|
339
|
+
`,
|
|
340
|
+
},
|
|
341
|
+
// Global styles
|
|
342
|
+
{
|
|
343
|
+
path: "styles/globals.css",
|
|
344
|
+
content: `@tailwind base;
|
|
345
|
+
@tailwind components;
|
|
346
|
+
@tailwind utilities;
|
|
347
|
+
|
|
348
|
+
@layer base {
|
|
349
|
+
:root {
|
|
350
|
+
--background: 0 0% 100%;
|
|
351
|
+
--foreground: 222.2 84% 4.9%;
|
|
352
|
+
--card: 0 0% 100%;
|
|
353
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
354
|
+
--popover: 0 0% 100%;
|
|
355
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
356
|
+
--primary: 222.2 47.4% 11.2%;
|
|
357
|
+
--primary-foreground: 210 40% 98%;
|
|
358
|
+
--secondary: 210 40% 96.1%;
|
|
359
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
360
|
+
--muted: 210 40% 96.1%;
|
|
361
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
362
|
+
--accent: 210 40% 96.1%;
|
|
363
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
364
|
+
--destructive: 0 84.2% 60.2%;
|
|
365
|
+
--destructive-foreground: 210 40% 98%;
|
|
366
|
+
--border: 214.3 31.8% 91.4%;
|
|
367
|
+
--input: 214.3 31.8% 91.4%;
|
|
368
|
+
--ring: 222.2 84% 4.9%;
|
|
369
|
+
--radius: 0.5rem;
|
|
370
|
+
--chart-1: 12 76% 61%;
|
|
371
|
+
--chart-2: 173 58% 39%;
|
|
372
|
+
--chart-3: 197 37% 24%;
|
|
373
|
+
--chart-4: 43 74% 66%;
|
|
374
|
+
--chart-5: 27 87% 67%;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.dark {
|
|
378
|
+
--background: 222.2 84% 4.9%;
|
|
379
|
+
--foreground: 210 40% 98%;
|
|
380
|
+
--card: 222.2 84% 4.9%;
|
|
381
|
+
--card-foreground: 210 40% 98%;
|
|
382
|
+
--popover: 222.2 84% 4.9%;
|
|
383
|
+
--popover-foreground: 210 40% 98%;
|
|
384
|
+
--primary: 210 40% 98%;
|
|
385
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
386
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
387
|
+
--secondary-foreground: 210 40% 98%;
|
|
388
|
+
--muted: 217.2 32.6% 17.5%;
|
|
389
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
390
|
+
--accent: 217.2 32.6% 17.5%;
|
|
391
|
+
--accent-foreground: 210 40% 98%;
|
|
392
|
+
--destructive: 0 62.8% 30.6%;
|
|
393
|
+
--destructive-foreground: 210 40% 98%;
|
|
394
|
+
--border: 217.2 32.6% 17.5%;
|
|
395
|
+
--input: 217.2 32.6% 17.5%;
|
|
396
|
+
--ring: 212.7 26.8% 83.9%;
|
|
397
|
+
--chart-1: 220 70% 50%;
|
|
398
|
+
--chart-2: 160 60% 45%;
|
|
399
|
+
--chart-3: 30 80% 55%;
|
|
400
|
+
--chart-4: 280 65% 60%;
|
|
401
|
+
--chart-5: 340 75% 55%;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
@layer base {
|
|
406
|
+
* {
|
|
407
|
+
@apply border-border;
|
|
408
|
+
}
|
|
409
|
+
body {
|
|
410
|
+
@apply bg-background text-foreground;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* Utility classes for Storybook stories */
|
|
415
|
+
.story-container {
|
|
416
|
+
@apply p-6;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.story-grid {
|
|
420
|
+
@apply grid gap-4;
|
|
421
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.story-row {
|
|
425
|
+
@apply flex gap-4 flex-wrap items-center;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.story-stack {
|
|
429
|
+
@apply flex flex-col gap-4;
|
|
430
|
+
}
|
|
431
|
+
`,
|
|
432
|
+
},
|
|
433
|
+
// Lib utilities
|
|
434
|
+
{
|
|
435
|
+
path: "lib/utils.ts",
|
|
436
|
+
content: `import { type ClassValue, clsx } from "clsx";
|
|
437
|
+
import { twMerge } from "tailwind-merge";
|
|
438
|
+
|
|
439
|
+
export function cn(...inputs: ClassValue[]) {
|
|
440
|
+
return twMerge(clsx(inputs));
|
|
441
|
+
}
|
|
442
|
+
`,
|
|
443
|
+
},
|
|
444
|
+
// Next.js mocks for Storybook
|
|
445
|
+
{
|
|
446
|
+
path: "lib/next-mocks.ts",
|
|
447
|
+
content: `// Mock implementations for Next.js features in Storybook
|
|
448
|
+
// These mocks allow components that use Next.js features to render in Storybook
|
|
449
|
+
|
|
450
|
+
import * as React from "react";
|
|
451
|
+
|
|
452
|
+
// next/navigation mocks
|
|
453
|
+
export const useRouter = () => ({
|
|
454
|
+
push: () => {},
|
|
455
|
+
replace: () => {},
|
|
456
|
+
prefetch: () => {},
|
|
457
|
+
back: () => {},
|
|
458
|
+
forward: () => {},
|
|
459
|
+
refresh: () => {},
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
export const usePathname = () => "/";
|
|
463
|
+
export const useSearchParams = () => new URLSearchParams();
|
|
464
|
+
export const useParams = () => ({});
|
|
465
|
+
|
|
466
|
+
// next/link mock - just renders an anchor tag
|
|
467
|
+
interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
468
|
+
href: string;
|
|
469
|
+
children?: React.ReactNode;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
|
|
473
|
+
({ href, children, ...props }, ref) => {
|
|
474
|
+
return React.createElement("a", { ...props, href, ref }, children);
|
|
475
|
+
}
|
|
476
|
+
);
|
|
477
|
+
Link.displayName = "Link";
|
|
478
|
+
|
|
479
|
+
// next/dynamic mock - returns the component directly
|
|
480
|
+
function dynamicImpl<T>(
|
|
481
|
+
loader: () => Promise<{ default: React.ComponentType<T> }>,
|
|
482
|
+
options?: { loading?: () => React.ReactNode; ssr?: boolean }
|
|
483
|
+
) {
|
|
484
|
+
const LazyComponent = React.lazy(loader);
|
|
485
|
+
|
|
486
|
+
return function DynamicComponent(props: T) {
|
|
487
|
+
return React.createElement(
|
|
488
|
+
React.Suspense,
|
|
489
|
+
{ fallback: options?.loading?.() ?? null },
|
|
490
|
+
React.createElement(LazyComponent, props as React.Attributes & T)
|
|
491
|
+
);
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export { dynamicImpl as dynamic };
|
|
496
|
+
export default dynamicImpl;
|
|
497
|
+
`,
|
|
498
|
+
},
|
|
499
|
+
// Introduction story
|
|
500
|
+
{
|
|
501
|
+
path: "stories/Introduction.mdx",
|
|
502
|
+
content: `{/* Introduction.mdx */}
|
|
503
|
+
import { Meta } from "@storybook/blocks";
|
|
504
|
+
|
|
505
|
+
<Meta title="Introduction" />
|
|
506
|
+
|
|
507
|
+
# Welcome to Your Design System
|
|
508
|
+
|
|
509
|
+
This is your project's Storybook - a living design system that documents your UI components.
|
|
510
|
+
|
|
511
|
+
## Getting Started
|
|
512
|
+
|
|
513
|
+
1. **Install dependencies**: Run \`npm install\` in this folder
|
|
514
|
+
2. **Add your first component** in the \`components/\` folder
|
|
515
|
+
3. **Create a story** in the same folder to document it
|
|
516
|
+
4. **Run Storybook** with \`npm run dev\`
|
|
517
|
+
|
|
518
|
+
## How to Create Components
|
|
519
|
+
|
|
520
|
+
Create a component file like \`components/ui/Button.tsx\`:
|
|
521
|
+
|
|
522
|
+
\`\`\`tsx
|
|
523
|
+
import * as React from "react";
|
|
524
|
+
import { cn } from "@/lib/utils";
|
|
525
|
+
|
|
526
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
527
|
+
variant?: "default" | "destructive" | "outline";
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function Button({ className, variant = "default", ...props }: ButtonProps) {
|
|
531
|
+
return (
|
|
532
|
+
<button
|
|
533
|
+
className={cn(
|
|
534
|
+
"px-4 py-2 rounded-md font-medium transition-colors",
|
|
535
|
+
variant === "default" && "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
536
|
+
variant === "destructive" && "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
537
|
+
variant === "outline" && "border border-input bg-background hover:bg-accent",
|
|
538
|
+
className
|
|
539
|
+
)}
|
|
540
|
+
{...props}
|
|
541
|
+
/>
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
\`\`\`
|
|
545
|
+
|
|
546
|
+
Then create a story file \`components/ui/Button.stories.tsx\`:
|
|
547
|
+
|
|
548
|
+
\`\`\`tsx
|
|
549
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
550
|
+
import { Button } from "./Button";
|
|
551
|
+
|
|
552
|
+
const meta: Meta<typeof Button> = {
|
|
553
|
+
title: "Components/Button",
|
|
554
|
+
component: Button,
|
|
555
|
+
tags: ["autodocs"],
|
|
556
|
+
argTypes: {
|
|
557
|
+
variant: {
|
|
558
|
+
control: "select",
|
|
559
|
+
options: ["default", "destructive", "outline"],
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
export default meta;
|
|
565
|
+
type Story = StoryObj<typeof Button>;
|
|
566
|
+
|
|
567
|
+
export const Default: Story = {
|
|
568
|
+
args: {
|
|
569
|
+
children: "Click me",
|
|
570
|
+
variant: "default",
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
export const Destructive: Story = {
|
|
575
|
+
args: {
|
|
576
|
+
children: "Delete",
|
|
577
|
+
variant: "destructive",
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
export const Outline: Story = {
|
|
582
|
+
args: {
|
|
583
|
+
children: "Cancel",
|
|
584
|
+
variant: "outline",
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
\`\`\`
|
|
588
|
+
|
|
589
|
+
## Project Structure
|
|
590
|
+
|
|
591
|
+
\`\`\`
|
|
592
|
+
storybook/
|
|
593
|
+
├── .storybook/ # Storybook configuration
|
|
594
|
+
│ ├── main.ts # Addons and build config
|
|
595
|
+
│ └── preview.ts # Global decorators and parameters
|
|
596
|
+
├── components/ # Your components (create this folder)
|
|
597
|
+
│ └── ui/ # UI components + stories
|
|
598
|
+
├── lib/
|
|
599
|
+
│ ├── utils.ts # Utility functions (cn)
|
|
600
|
+
│ └── next-mocks.ts # Next.js mocks for Storybook
|
|
601
|
+
├── stories/
|
|
602
|
+
│ └── Introduction.mdx # This file
|
|
603
|
+
├── styles/
|
|
604
|
+
│ └── globals.css # Tailwind + design tokens
|
|
605
|
+
├── middleware.js # EpicContext authentication
|
|
606
|
+
├── tailwind.config.ts # Tailwind configuration
|
|
607
|
+
└── package.json
|
|
608
|
+
\`\`\`
|
|
609
|
+
|
|
610
|
+
## Design Tokens
|
|
611
|
+
|
|
612
|
+
All tokens are defined in \`styles/globals.css\` using CSS custom properties:
|
|
613
|
+
|
|
614
|
+
- **Colors**: Primary, secondary, muted, accent, destructive
|
|
615
|
+
- **Backgrounds**: background, card, popover
|
|
616
|
+
- **Border**: border, input, ring
|
|
617
|
+
- **Radius**: lg, md, sm
|
|
618
|
+
|
|
619
|
+
Light and dark themes are fully supported. Customize these tokens to match your brand.
|
|
620
|
+
|
|
621
|
+
## Authentication
|
|
622
|
+
|
|
623
|
+
This Storybook is protected by EpicContext authentication. Users must be logged into your EpicContext organization to view it when deployed.
|
|
624
|
+
|
|
625
|
+
### Local Development
|
|
626
|
+
|
|
627
|
+
During local development (\`npm run dev\`), authentication is bypassed so you can work freely.
|
|
628
|
+
|
|
629
|
+
### Deploying to Vercel
|
|
630
|
+
|
|
631
|
+
1. Push this folder to a GitHub repository
|
|
632
|
+
2. Connect it to Vercel
|
|
633
|
+
3. Set environment variables:
|
|
634
|
+
- \`EPICCONTEXT_STORYBOOK_SECRET\` - Get from EpicContext settings
|
|
635
|
+
- \`EPICCONTEXT_APP_URL\` - Your EpicContext instance URL
|
|
636
|
+
4. Deploy!
|
|
637
|
+
|
|
638
|
+
The middleware will automatically protect your Storybook and redirect unauthenticated users to log in.
|
|
639
|
+
|
|
640
|
+
## Next Steps
|
|
641
|
+
|
|
642
|
+
- [ ] Create a \`components/ui\` folder
|
|
643
|
+
- [ ] Add your first component (Button is a great start)
|
|
644
|
+
- [ ] Document it with a story file
|
|
645
|
+
- [ ] Deploy to Vercel
|
|
646
|
+
- [ ] Link the URL in EpicContext Design System section
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
Built with [Storybook](https://storybook.js.org) | Secured by [EpicContext](https://epiccontext.com)
|
|
651
|
+
`,
|
|
652
|
+
},
|
|
653
|
+
// Post-build script for Vercel
|
|
654
|
+
{
|
|
655
|
+
path: "scripts/postbuild.mjs",
|
|
656
|
+
content: `/**
|
|
657
|
+
* Post-build script for Vercel Build Output API v3
|
|
658
|
+
*
|
|
659
|
+
* This script restructures the Storybook build output into Vercel's
|
|
660
|
+
* Build Output API format, enabling Edge Middleware for authentication.
|
|
661
|
+
*/
|
|
662
|
+
|
|
663
|
+
import fs from "fs";
|
|
664
|
+
import path from "path";
|
|
665
|
+
import { fileURLToPath } from "url";
|
|
666
|
+
|
|
667
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
668
|
+
const rootDir = path.resolve(__dirname, "..");
|
|
669
|
+
const storybookOutput = path.join(rootDir, "storybook-static");
|
|
670
|
+
const vercelOutput = path.join(rootDir, ".vercel", "output");
|
|
671
|
+
|
|
672
|
+
console.log("Setting up Vercel Build Output API v3...");
|
|
673
|
+
|
|
674
|
+
// Clean and create output directory
|
|
675
|
+
if (fs.existsSync(vercelOutput)) {
|
|
676
|
+
fs.rmSync(vercelOutput, { recursive: true });
|
|
677
|
+
}
|
|
678
|
+
fs.mkdirSync(path.join(vercelOutput, "static"), { recursive: true });
|
|
679
|
+
fs.mkdirSync(path.join(vercelOutput, "functions", "middleware.func"), {
|
|
680
|
+
recursive: true,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// Copy static files recursively
|
|
684
|
+
console.log("Copying static files...");
|
|
685
|
+
const copyRecursive = (src, dest) => {
|
|
686
|
+
if (!fs.existsSync(src)) return;
|
|
687
|
+
|
|
688
|
+
if (fs.statSync(src).isDirectory()) {
|
|
689
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
690
|
+
for (const file of fs.readdirSync(src)) {
|
|
691
|
+
copyRecursive(path.join(src, file), path.join(dest, file));
|
|
692
|
+
}
|
|
693
|
+
} else {
|
|
694
|
+
fs.copyFileSync(src, dest);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
copyRecursive(storybookOutput, path.join(vercelOutput, "static"));
|
|
699
|
+
|
|
700
|
+
// Copy middleware function
|
|
701
|
+
console.log("Setting up Edge Middleware...");
|
|
702
|
+
fs.copyFileSync(
|
|
703
|
+
path.join(rootDir, "middleware.js"),
|
|
704
|
+
path.join(vercelOutput, "functions", "middleware.func", "index.js")
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
// Write the function config
|
|
708
|
+
fs.writeFileSync(
|
|
709
|
+
path.join(vercelOutput, "functions", "middleware.func", ".vc-config.json"),
|
|
710
|
+
JSON.stringify(
|
|
711
|
+
{
|
|
712
|
+
runtime: "edge",
|
|
713
|
+
entrypoint: "index.js",
|
|
714
|
+
},
|
|
715
|
+
null,
|
|
716
|
+
2
|
|
717
|
+
)
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
// Create the main config.json
|
|
721
|
+
console.log("Writing config.json...");
|
|
722
|
+
fs.writeFileSync(
|
|
723
|
+
path.join(vercelOutput, "config.json"),
|
|
724
|
+
JSON.stringify(
|
|
725
|
+
{
|
|
726
|
+
version: 3,
|
|
727
|
+
routes: [
|
|
728
|
+
// Static assets - bypass middleware, serve directly
|
|
729
|
+
{
|
|
730
|
+
src: "^/assets/.*",
|
|
731
|
+
headers: { "Cache-Control": "public, max-age=31536000, immutable" },
|
|
732
|
+
continue: true,
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
src: "^/sb-.*",
|
|
736
|
+
continue: true,
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
src: ".*\\\\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|json|map)$",
|
|
740
|
+
continue: true,
|
|
741
|
+
},
|
|
742
|
+
// HTML requests go through middleware
|
|
743
|
+
{
|
|
744
|
+
src: "^/(?!assets|sb-).*$",
|
|
745
|
+
middlewarePath: "middleware",
|
|
746
|
+
continue: true,
|
|
747
|
+
},
|
|
748
|
+
// Serve static files
|
|
749
|
+
{ handle: "filesystem" },
|
|
750
|
+
// Fallback to index.html for SPA routing
|
|
751
|
+
{
|
|
752
|
+
src: "/(.*)",
|
|
753
|
+
dest: "/index.html",
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
},
|
|
757
|
+
null,
|
|
758
|
+
2
|
|
759
|
+
)
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
console.log("Vercel Build Output API setup complete!");
|
|
763
|
+
console.log(" Static files: .vercel/output/static");
|
|
764
|
+
console.log(" Middleware: .vercel/output/functions/middleware.func");
|
|
765
|
+
console.log(" Config: .vercel/output/config.json");
|
|
766
|
+
`,
|
|
767
|
+
},
|
|
768
|
+
// Authentication middleware
|
|
769
|
+
{
|
|
770
|
+
path: "middleware.js",
|
|
771
|
+
content: `/**
|
|
772
|
+
* EpicContext Storybook Authentication Middleware
|
|
773
|
+
*
|
|
774
|
+
* Vercel Edge Middleware for non-Next.js projects.
|
|
775
|
+
* Protects Storybook by validating JWT tokens from EpicContext.
|
|
776
|
+
*/
|
|
777
|
+
|
|
778
|
+
export const config = {
|
|
779
|
+
// Match all paths - we'll filter inside the middleware
|
|
780
|
+
matcher: ["/(.*)", "/"],
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
const COOKIE_NAME = "epiccontext_storybook_token";
|
|
784
|
+
const COOKIE_MAX_AGE = 60 * 60 * 24; // 24 hours
|
|
785
|
+
|
|
786
|
+
function base64UrlDecode(data) {
|
|
787
|
+
const padded = data + "=".repeat((4 - (data.length % 4)) % 4);
|
|
788
|
+
return atob(padded.replace(/-/g, "+").replace(/_/g, "/"));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function createSignature(data, secret) {
|
|
792
|
+
const encoder = new TextEncoder();
|
|
793
|
+
const keyData = encoder.encode(secret);
|
|
794
|
+
const messageData = encoder.encode(data);
|
|
795
|
+
|
|
796
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
797
|
+
"raw",
|
|
798
|
+
keyData,
|
|
799
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
800
|
+
false,
|
|
801
|
+
["sign"]
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
|
|
805
|
+
const base64 = btoa(String.fromCharCode(...new Uint8Array(signature)));
|
|
806
|
+
return base64.replace(/\\+/g, "-").replace(/\\//g, "_").replace(/=/g, "");
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
async function deriveSecret(secret) {
|
|
810
|
+
const encoder = new TextEncoder();
|
|
811
|
+
const data = encoder.encode(\`epiccontext-storybook:\${secret}\`);
|
|
812
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
813
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
814
|
+
return Array.from(hashArray)
|
|
815
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
816
|
+
.join("");
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async function validateToken(token, secret, expectedAudience) {
|
|
820
|
+
try {
|
|
821
|
+
const parts = token.split(".");
|
|
822
|
+
if (parts.length !== 3) {
|
|
823
|
+
return { valid: false, error: "Invalid token format" };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const [encodedHeader, encodedPayload, signature] = parts;
|
|
827
|
+
const derivedSecret = await deriveSecret(secret);
|
|
828
|
+
|
|
829
|
+
const expectedSignature = await createSignature(
|
|
830
|
+
\`\${encodedHeader}.\${encodedPayload}\`,
|
|
831
|
+
derivedSecret
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
if (signature !== expectedSignature) {
|
|
835
|
+
return { valid: false, error: "Invalid signature" };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const payload = JSON.parse(base64UrlDecode(encodedPayload));
|
|
839
|
+
|
|
840
|
+
if (payload.iss !== "epiccontext") {
|
|
841
|
+
return { valid: false, error: "Invalid issuer" };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const now = Math.floor(Date.now() / 1000);
|
|
845
|
+
if (payload.exp < now) {
|
|
846
|
+
return { valid: false, error: "Token expired" };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (expectedAudience && payload.aud !== expectedAudience) {
|
|
850
|
+
return { valid: false, error: "Invalid audience" };
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return { valid: true, payload };
|
|
854
|
+
} catch (e) {
|
|
855
|
+
return { valid: false, error: "Token parsing failed" };
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function getEpicContextUrl() {
|
|
860
|
+
try {
|
|
861
|
+
return process.env.EPICCONTEXT_APP_URL || "https://epic-context.vercel.app";
|
|
862
|
+
} catch {
|
|
863
|
+
return "https://epic-context.vercel.app";
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function getSecret() {
|
|
868
|
+
try {
|
|
869
|
+
return process.env.EPICCONTEXT_STORYBOOK_SECRET || null;
|
|
870
|
+
} catch {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function parseCookies(cookieHeader) {
|
|
876
|
+
if (!cookieHeader) return {};
|
|
877
|
+
return Object.fromEntries(
|
|
878
|
+
cookieHeader.split(";").map((cookie) => {
|
|
879
|
+
const [key, ...val] = cookie.trim().split("=");
|
|
880
|
+
return [key, val.join("=")];
|
|
881
|
+
})
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Static file extensions that should bypass authentication
|
|
886
|
+
const STATIC_EXTENSIONS = [
|
|
887
|
+
'.js', '.css', '.json', '.png', '.jpg', '.jpeg', '.gif', '.svg',
|
|
888
|
+
'.ico', '.woff', '.woff2', '.ttf', '.eot', '.map', '.txt'
|
|
889
|
+
];
|
|
890
|
+
|
|
891
|
+
// Paths that should bypass authentication
|
|
892
|
+
const BYPASS_PATHS = [
|
|
893
|
+
'/assets/', '/sb-', '/static/', '/_next/', '/favicon'
|
|
894
|
+
];
|
|
895
|
+
|
|
896
|
+
function shouldBypassAuth(pathname) {
|
|
897
|
+
const hasStaticExtension = STATIC_EXTENSIONS.some(ext => pathname.endsWith(ext));
|
|
898
|
+
if (hasStaticExtension) return true;
|
|
899
|
+
|
|
900
|
+
const isBypassPath = BYPASS_PATHS.some(path => pathname.startsWith(path));
|
|
901
|
+
if (isBypassPath) return true;
|
|
902
|
+
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
export default async function middleware(request) {
|
|
907
|
+
const url = new URL(request.url);
|
|
908
|
+
const pathname = url.pathname;
|
|
909
|
+
|
|
910
|
+
// Skip authentication for static files and internal Storybook paths
|
|
911
|
+
if (shouldBypassAuth(pathname)) {
|
|
912
|
+
return undefined;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const secret = getSecret();
|
|
916
|
+
|
|
917
|
+
// If no secret configured, allow access (development mode)
|
|
918
|
+
if (!secret) {
|
|
919
|
+
console.warn(
|
|
920
|
+
"EPICCONTEXT_STORYBOOK_SECRET not set - Storybook is unprotected"
|
|
921
|
+
);
|
|
922
|
+
return undefined;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const hostname = url.hostname;
|
|
926
|
+
|
|
927
|
+
// Check for token in URL params
|
|
928
|
+
const tokenFromUrl = url.searchParams.get("token");
|
|
929
|
+
|
|
930
|
+
// Check for token in cookie
|
|
931
|
+
const cookieHeader = request.headers.get("cookie");
|
|
932
|
+
const cookies = parseCookies(cookieHeader);
|
|
933
|
+
const tokenFromCookie = cookies[COOKIE_NAME];
|
|
934
|
+
|
|
935
|
+
const token = tokenFromUrl || tokenFromCookie;
|
|
936
|
+
|
|
937
|
+
if (!token) {
|
|
938
|
+
// No token - redirect to EpicContext login
|
|
939
|
+
const epicContextUrl = getEpicContextUrl();
|
|
940
|
+
const redirectUrl = new URL("/auth/storybook", epicContextUrl);
|
|
941
|
+
redirectUrl.searchParams.set("redirect", request.url);
|
|
942
|
+
|
|
943
|
+
return Response.redirect(redirectUrl.toString(), 302);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Validate the token
|
|
947
|
+
const result = await validateToken(token, secret, hostname);
|
|
948
|
+
|
|
949
|
+
if (!result.valid) {
|
|
950
|
+
// Invalid token - redirect to EpicContext with error
|
|
951
|
+
const epicContextUrl = getEpicContextUrl();
|
|
952
|
+
const errorUrl = new URL("/auth/storybook/error", epicContextUrl);
|
|
953
|
+
errorUrl.searchParams.set("error", result.error || "Invalid token");
|
|
954
|
+
errorUrl.searchParams.set("redirect", request.url);
|
|
955
|
+
|
|
956
|
+
return Response.redirect(errorUrl.toString(), 302);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Token is valid
|
|
960
|
+
// If token came from URL, set cookie and redirect to clean URL
|
|
961
|
+
if (tokenFromUrl && !tokenFromCookie) {
|
|
962
|
+
const cleanUrl = new URL(request.url);
|
|
963
|
+
cleanUrl.searchParams.delete("token");
|
|
964
|
+
|
|
965
|
+
const isSecure = url.protocol === "https:";
|
|
966
|
+
const cookieValue = \`\${COOKIE_NAME}=\${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=\${COOKIE_MAX_AGE}\${isSecure ? "; Secure" : ""}\`;
|
|
967
|
+
|
|
968
|
+
return new Response(null, {
|
|
969
|
+
status: 302,
|
|
970
|
+
headers: {
|
|
971
|
+
"Location": cleanUrl.toString(),
|
|
972
|
+
"Set-Cookie": cookieValue,
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Continue to static files
|
|
978
|
+
return undefined;
|
|
979
|
+
}
|
|
980
|
+
`,
|
|
981
|
+
},
|
|
982
|
+
// Empty public folder placeholder
|
|
983
|
+
{
|
|
984
|
+
path: "public/.gitkeep",
|
|
985
|
+
content: `# This folder contains static assets for Storybook
|
|
986
|
+
# Add images, fonts, or other assets here
|
|
987
|
+
`,
|
|
988
|
+
},
|
|
989
|
+
];
|
|
990
|
+
}
|
|
991
|
+
//# sourceMappingURL=storybook.js.map
|