create-nextblock 0.0.3 → 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.
@@ -131,7 +131,7 @@ async function handleCommand(projectDirectory, options) {
131
131
  console.log(chalk.green('Global styles configured.'));
132
132
 
133
133
  await sanitizeTailwindConfig(projectDir);
134
- console.log(chalk.green('tailwind.config.ts sanitized.'));
134
+ console.log(chalk.green('tailwind.config.js sanitized.'));
135
135
 
136
136
  await normalizeTsconfig(projectDir);
137
137
  console.log(chalk.green('tsconfig.json normalized.'));
@@ -598,10 +598,9 @@ async function ensureEditorStyles(projectDir) {
598
598
  }
599
599
 
600
600
  async function sanitizeTailwindConfig(projectDir) {
601
- const tailwindConfigPath = resolve(projectDir, 'tailwind.config.ts');
602
- const content = `import type { Config } from 'tailwindcss';
603
-
604
- const config = {
601
+ const tailwindConfigPath = resolve(projectDir, 'tailwind.config.js');
602
+ const content = `/** @type {import('tailwindcss').Config} */
603
+ module.exports = {
605
604
  darkMode: ['class'],
606
605
  content: [
607
606
  './app/**/*.{js,ts,jsx,tsx,mdx}',
@@ -693,9 +692,7 @@ const config = {
693
692
  },
694
693
  },
695
694
  plugins: [require('tailwindcss-animate')],
696
- } satisfies Config;
697
-
698
- export default config;
695
+ };
699
696
  `;
700
697
 
701
698
  await fs.writeFile(tailwindConfigPath, content);
@@ -994,4 +991,3 @@ function buildNextConfigContent(editorUtilNames) {
994
991
 
995
992
  return lines.join('\n');
996
993
  }
997
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nextblock",
3
- "version": "0.0.3",
3
+ "version": "0.1.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -129,13 +129,13 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
129
129
  debouncedSave(updatedBlock);
130
130
  };
131
131
 
132
- const DynamicTextBlockEditor = dynamic(() => import('../editors/TextBlockEditor.tsx').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
133
- const DynamicHeadingBlockEditor = dynamic(() => import('../editors/HeadingBlockEditor.tsx').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
134
- const DynamicImageBlockEditor = dynamic(() => import('../editors/ImageBlockEditor.tsx').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
135
- const DynamicButtonBlockEditor = dynamic(() => import('../editors/ButtonBlockEditor.tsx').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
136
- const DynamicPostsGridBlockEditor = dynamic(() => import('../editors/PostsGridBlockEditor.tsx').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
137
- const DynamicVideoEmbedBlockEditor = dynamic(() => import('../editors/VideoEmbedBlockEditor.tsx').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
138
- const DynamicSectionBlockEditor = dynamic(() => import('../editors/SectionBlockEditor.tsx').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
132
+ const DynamicTextBlockEditor = dynamic(() => import('../editors/TextBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
133
+ const DynamicHeadingBlockEditor = dynamic(() => import('../editors/HeadingBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
134
+ const DynamicImageBlockEditor = dynamic(() => import('../editors/ImageBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
135
+ const DynamicButtonBlockEditor = dynamic(() => import('../editors/ButtonBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
136
+ const DynamicPostsGridBlockEditor = dynamic(() => import('../editors/PostsGridBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
137
+ const DynamicVideoEmbedBlockEditor = dynamic(() => import('../editors/VideoEmbedBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
138
+ const DynamicSectionBlockEditor = dynamic(() => import('../editors/SectionBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
139
139
 
140
140
  useEffect(() => {
141
141
  if (editingNestedBlockInfo) {
@@ -444,7 +444,7 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
444
444
  onContentChange={handleContentChange}
445
445
  onDelete={async (blockIdToDelete) => {
446
446
  startTransition(async () => {
447
- const result = await import("../actions.ts").then(({ deleteBlock }) =>
447
+ const result = await import("../actions").then(({ deleteBlock }) =>
448
448
  deleteBlock(
449
449
  blockIdToDelete,
450
450
  parentType === "page" ? parentId : null,
@@ -1,15 +1,14 @@
1
- /*─────────────────────────────────────────────────────────────────────────────
2
- globals.css — complete rewrite
3
- ─────────────────────────────────────────────────────────────────────────────*/
4
-
5
- @tailwind base;
6
- @tailwind components;
7
- @tailwind utilities;
8
-
1
+ @config "../../../../tailwind.config.js";
2
+ @import "tailwindcss/preflight";
3
+ @import "tailwindcss/theme";
4
+ @import "tailwindcss/utilities";
5
+ /*─────────────────────────────────────────────────────────────────────────────
6
+ globals.css — complete rewrite
7
+ ─────────────────────────────────────────────────────────────────────────────*/
9
8
  /*─────────────────────────────────────────────────────────────────────────────
10
9
  1. Critical CSS Resets (formerly in <style id="critical-css">)
11
10
  ─────────────────────────────────────────────────────────────────────────────*/
12
- @layer base {
11
+ @layer base {
13
12
  *, ::before, ::after {
14
13
  box-sizing: border-box;
15
14
  border-width: 0;
@@ -194,67 +193,103 @@
194
193
  ───────────────────────────────────────────────────────────────────────────*/
195
194
  /* Apply your border color everywhere */
196
195
  * {
197
- @apply border-border;
196
+ border-color: hsl(var(--border));
198
197
  }
199
198
 
200
199
  /* Body background & text */
201
200
  body {
202
- @apply bg-background text-foreground;
201
+ background-color: hsl(var(--background));
202
+ color: hsl(var(--foreground));
203
203
  }
204
204
 
205
205
  /* Semantic headings */
206
- ul, ol {
207
- @apply pl-6 mb-4;
206
+ ul,
207
+ ol {
208
+ padding-left: 1.5rem;
209
+ margin-bottom: 1rem;
208
210
  }
209
211
  li {
210
- @apply mb-1;
212
+ margin-bottom: 0.25rem;
211
213
  }
212
214
  blockquote {
213
- @apply p-4 italic border-l-4 border-border bg-muted text-muted-foreground mb-4;
215
+ padding: 1rem;
216
+ font-style: italic;
217
+ border-left-width: 4px;
218
+ border-left-style: solid;
219
+ border-left-color: hsl(var(--border));
220
+ background-color: hsl(var(--muted));
221
+ color: hsl(var(--muted-foreground));
222
+ margin-bottom: 1rem;
214
223
  }
215
224
  code {
216
- @apply bg-muted text-muted-foreground px-1 py-0.5 rounded-sm font-mono text-sm;
225
+ background-color: hsl(var(--muted));
226
+ color: hsl(var(--muted-foreground));
227
+ padding: 0.25rem 0.25rem;
228
+ padding-top: 0.125rem;
229
+ padding-bottom: 0.125rem;
230
+ border-radius: 0.125rem;
231
+ font-family:
232
+ ui-monospace,
233
+ SFMono-Regular,
234
+ Menlo,
235
+ Monaco,
236
+ Consolas,
237
+ 'Liberation Mono',
238
+ 'Courier New',
239
+ monospace;
240
+ font-size: 0.875rem;
241
+ line-height: 1.25rem;
217
242
  }
218
243
  pre {
219
- @apply bg-muted p-4 rounded-md overflow-x-auto mb-4;
244
+ background-color: hsl(var(--muted));
245
+ padding: 1rem;
246
+ border-radius: 0.375rem;
247
+ overflow-x: auto;
248
+ margin-bottom: 1rem;
220
249
  }
221
250
 
222
251
  table {
223
- @apply w-full border-collapse mb-4;
252
+ width: 100%;
253
+ border-collapse: collapse;
254
+ margin-bottom: 1rem;
224
255
  }
225
256
  thead {
226
- @apply bg-muted;
257
+ background-color: hsl(var(--muted));
227
258
  }
228
- th, td {
229
- @apply border border-border p-2 text-left;
259
+ th,
260
+ td {
261
+ border: 1px solid hsl(var(--border));
262
+ padding: 0.5rem;
263
+ text-align: left;
230
264
  }
231
265
  th {
232
- @apply font-semibold;
266
+ font-weight: 600;
233
267
  }
234
268
  hr {
235
- @apply border-t border-border my-8;
236
- }
237
- }
238
-
239
- /* Image alignment preserved from editor output */
240
- @layer components {
241
- img[data-align='left'] {
242
- display: block;
243
- margin-left: 0;
244
- margin-right: auto;
245
- }
246
- img[data-align='right'] {
247
- display: block;
248
- margin-left: auto;
249
- margin-right: 0;
250
- }
251
- img[data-align='center'] {
252
- display: block;
253
- margin-left: auto;
254
- margin-right: auto;
255
- }
256
- }
257
-
269
+ border-top: 1px solid hsl(var(--border));
270
+ margin: 2rem 0;
271
+ }
272
+ }
273
+
274
+ /* Image alignment preserved from editor output */
275
+ @layer components {
276
+ img[data-align='left'] {
277
+ display: block;
278
+ margin-left: 0;
279
+ margin-right: auto;
280
+ }
281
+ img[data-align='right'] {
282
+ display: block;
283
+ margin-left: auto;
284
+ margin-right: 0;
285
+ }
286
+ img[data-align='center'] {
287
+ display: block;
288
+ margin-left: auto;
289
+ margin-right: auto;
290
+ }
291
+ }
292
+
258
293
  /*─────────────────────────────────────────────────────────────────────────────
259
294
  3. Theme color variables
260
295
  ─────────────────────────────────────────────────────────────────────────────*/
@@ -1,23 +1,23 @@
1
- import React from "react";
2
- import Link from "next/link";
1
+ import React from "react";
2
+ import Link from "next/link";
3
3
  export type ButtonBlockContent = {
4
4
  text?: string;
5
5
  url?: string;
6
6
  variant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'link';
7
- size?: 'default' | 'sm' | 'lg';
8
- };
9
-
10
- interface ButtonBlockRendererProps {
11
- content: ButtonBlockContent;
12
- languageId: number; // This prop seems unused
13
- }
14
-
7
+ size?: 'default' | 'sm' | 'lg';
8
+ };
9
+
10
+ interface ButtonBlockRendererProps {
11
+ content: ButtonBlockContent;
12
+ languageId: number; // This prop seems unused
13
+ }
14
+
15
15
  const ButtonBlockRenderer: React.FC<ButtonBlockRendererProps> = ({
16
16
  content,
17
17
  // languageId, // Unused
18
18
  }) => {
19
19
  const baseClasses =
20
- "inline-flex items-center justify-center rounded-md border text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50";
20
+ "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50";
21
21
  const variantClasses: Record<string, string> = {
22
22
  default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
23
23
  outline:
@@ -36,12 +36,12 @@ const ButtonBlockRenderer: React.FC<ButtonBlockRendererProps> = ({
36
36
  const isExternal =
37
37
  content.url?.startsWith("http") ||
38
38
  content.url?.startsWith("mailto:") ||
39
- content.url?.startsWith("tel:");
40
- const isAnchor = content.url?.startsWith("#");
41
-
42
- const buttonText = content.text || "Button";
43
- const buttonVariant = content.variant || "default";
44
- const buttonSize = content.size || "default";
39
+ content.url?.startsWith("tel:");
40
+ const isAnchor = content.url?.startsWith("#");
41
+
42
+ const buttonText = content.text || "Button";
43
+ const buttonVariant = content.variant || "default";
44
+ const buttonSize = content.size || "default";
45
45
 
46
46
  return (
47
47
  <div className="my-6 text-center">
@@ -88,5 +88,5 @@ const ButtonBlockRenderer: React.FC<ButtonBlockRendererProps> = ({
88
88
  </div>
89
89
  );
90
90
  };
91
-
91
+
92
92
  export default ButtonBlockRenderer;
@@ -1,9 +1,32 @@
1
1
  // components/blocks/renderers/HeroBlockRenderer.tsx
2
- import React from "react";
2
+ import React from "react";
3
3
  import type { SectionBlockContent, Gradient } from "../../../lib/blocks/blockRegistry";
4
4
  import Image from 'next/image';
5
5
 
6
- const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
6
+ const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
7
+ const HERO_BACKGROUND_DEFAULT_QUALITY = 60;
8
+
9
+ function resolveImageQuality(value: unknown, fallback: number): number {
10
+ if (value === undefined || value === null || value === '') {
11
+ return fallback;
12
+ }
13
+
14
+ const numeric =
15
+ typeof value === 'number'
16
+ ? value
17
+ : Number.parseInt(String(value), 10);
18
+
19
+ if (!Number.isFinite(numeric)) {
20
+ return fallback;
21
+ }
22
+
23
+ const rounded = Math.round(numeric);
24
+ if (rounded < 1 || rounded > 100) {
25
+ return fallback;
26
+ }
27
+
28
+ return rounded;
29
+ }
7
30
 
8
31
  interface SectionBlockRendererProps {
9
32
  content: SectionBlockContent;
@@ -169,13 +192,14 @@ const HeroBlockRenderer: React.FC<SectionBlockRendererProps> = ({
169
192
  content,
170
193
  languageId,
171
194
  }) => {
172
- const { styles, className: backgroundClassName } = generateBackgroundStyles(content.background);
173
-
174
- const backgroundImage = content.background.type === 'image' ? content.background.image : undefined;
175
-
176
- if (backgroundImage) {
177
- delete styles.backgroundImage;
178
- }
195
+ const { styles, className: backgroundClassName } = generateBackgroundStyles(content.background);
196
+
197
+ const backgroundImage = content.background.type === 'image' ? content.background.image : undefined;
198
+
199
+ if (backgroundImage) {
200
+ delete styles.backgroundImage;
201
+ }
202
+ const heroImageQuality = resolveImageQuality(backgroundImage?.quality, HERO_BACKGROUND_DEFAULT_QUALITY);
179
203
 
180
204
  // Build CSS classes
181
205
  const containerClass = containerClasses[content.container_type] || containerClasses.container;
@@ -205,13 +229,13 @@ const HeroBlockRenderer: React.FC<SectionBlockRendererProps> = ({
205
229
  objectFit: backgroundImage.size || 'cover',
206
230
  objectPosition: backgroundImage.position || 'center'
207
231
  }}
208
- sizes="(max-width: 768px) 100vw, (max-width: 1200px) 100vw, 100vw"
209
- priority={true}
210
- fetchPriority="high"
211
- quality={35}
212
- {...imageProps}
213
- />
214
- )}
232
+ sizes="(max-width: 768px) 100vw, (max-width: 1200px) 100vw, 100vw"
233
+ priority={true}
234
+ fetchPriority="high"
235
+ quality={heroImageQuality}
236
+ {...imageProps}
237
+ />
238
+ )}
215
239
  {backgroundImage?.overlay?.gradient && (
216
240
  <div
217
241
  className="absolute inset-0"
@@ -237,4 +261,4 @@ const HeroBlockRenderer: React.FC<SectionBlockRendererProps> = ({
237
261
  );
238
262
  };
239
263
 
240
- export default HeroBlockRenderer;
264
+ export default HeroBlockRenderer;
@@ -253,7 +253,7 @@
253
253
  │ └── blocks
254
254
  │ │ ├── README.md
255
255
  │ │ └── blockRegistry.ts
256
- │ ├── middleware.ts
256
+ │ ├── proxy.ts
257
257
  │ ├── next-env.d.ts
258
258
  │ ├── next.config.js
259
259
  │ ├── postcss.config.js
@@ -264,7 +264,7 @@
264
264
  │ ├── backfill-image-meta.ts
265
265
  │ ├── backup.js
266
266
  │ └── test-bundle-optimization.js
267
- │ ├── tailwind.config.ts
267
+ │ ├── tailwind.config.js
268
268
  │ └── tsconfig.json
269
269
  ├── components.json
270
270
  ├── docs
@@ -273,7 +273,7 @@
273
273
  ├── block-editor-analysis.md
274
274
  ├── inline-cta-widget-design.md
275
275
  ├── inline-widget-design.md
276
- └── monorepo-archetecture.md
276
+ └── monorepo-architecture.md
277
277
  ├── eslint.config.mjs
278
278
  ├── libs
279
279
  ├── db
@@ -285,7 +285,7 @@
285
285
  │ │ ├── lib
286
286
  │ │ │ └── supabase
287
287
  │ │ │ │ ├── client.ts
288
- │ │ │ │ ├── middleware.ts
288
+ │ │ │ │ ├── proxy.ts
289
289
  │ │ │ │ ├── server.ts
290
290
  │ │ │ │ ├── ssg-client.ts
291
291
  │ │ │ │ └── types.ts
@@ -422,5 +422,5 @@
422
422
  ├── package-lock.json
423
423
  ├── package.json
424
424
  ├── project.json
425
- ├── tailwind.config.ts
425
+ ├── tailwind.config.js
426
426
  └── tsconfig.base.json
@@ -1,25 +1,32 @@
1
- import { FlatCompat } from '@eslint/eslintrc';
2
- import { dirname } from 'path';
3
- import { fileURLToPath } from 'url';
4
- import js from '@eslint/js';
5
- import { fixupConfigRules } from '@eslint/compat';
6
1
  import nx from '@nx/eslint-plugin';
7
2
  import baseConfig from '../../eslint.config.mjs';
8
- const compat = new FlatCompat({
9
- baseDirectory: dirname(fileURLToPath(import.meta.url)),
10
- recommendedConfig: js.configs.recommended,
11
- });
3
+ import nextPlugin from '@next/eslint-plugin-next';
4
+
5
+ const nextRules = {
6
+ ...nextPlugin.configs.recommended.rules,
7
+ ...nextPlugin.configs['core-web-vitals'].rules,
8
+ };
12
9
 
13
10
  const config = [
14
- ...fixupConfigRules(compat.extends('next')),
15
- ...fixupConfigRules(compat.extends('next/core-web-vitals')),
16
11
  ...baseConfig,
17
12
  ...nx.configs['flat/react-typescript'],
18
13
  {
19
14
  ignores: ['.next/**/*', '**/next-env.d.ts', 'apps/nextblock/next-env.d.ts'],
20
15
  },
21
16
  {
17
+ files: [
18
+ '**/*.ts',
19
+ '**/*.tsx',
20
+ '**/*.js',
21
+ '**/*.jsx',
22
+ '**/*.mjs',
23
+ '**/*.cjs',
24
+ ],
25
+ plugins: {
26
+ '@next/next': nextPlugin,
27
+ },
22
28
  rules: {
29
+ ...nextRules,
23
30
  '@next/next/no-html-link-for-pages': ['error', 'apps/nextblock/app'],
24
31
  },
25
32
  },
@@ -117,20 +117,21 @@ export interface SectionBlockContent {
117
117
  solid_color?: string;
118
118
  min_height?: string;
119
119
  gradient?: Gradient;
120
- image?: {
121
- media_id: string;
122
- object_key: string;
123
- alt_text?: string;
124
- width?: number;
125
- height?: number;
126
- blur_data_url?: string;
127
- size: 'cover' | 'contain';
128
- position: 'center' | 'top' | 'bottom' | 'left' | 'right';
129
- overlay?: {
130
- type: 'gradient';
131
- gradient: Gradient;
132
- };
133
- };
120
+ image?: {
121
+ media_id: string;
122
+ object_key: string;
123
+ alt_text?: string;
124
+ width?: number;
125
+ height?: number;
126
+ blur_data_url?: string;
127
+ size: 'cover' | 'contain';
128
+ position: 'center' | 'top' | 'bottom' | 'left' | 'right';
129
+ quality?: number | null;
130
+ overlay?: {
131
+ type: 'gradient';
132
+ gradient: Gradient;
133
+ };
134
+ };
134
135
  };
135
136
  /** Responsive column configuration */
136
137
  responsive_columns: {
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- /// <reference path="./../../dist/apps/nextblock/.next/types/routes.d.ts" />
3
+ import "./.next/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -1,99 +1,66 @@
1
- //@ts-check
2
-
3
- const { composePlugins, withNx } = require('@nx/next');
4
-
5
- /**
6
- * @type {import('@nx/next/plugins/with-nx').WithNxOptions}
7
- **/
8
- const nextConfig = {
9
- // Use this to set Nx-specific options
10
- // See: https://nx.dev/recipes/next/next-config-setup
11
- nx: {svgr: false},
12
- env: {
13
- NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
14
- NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
15
- },
16
- images: {
17
- formats: ['image/avif', 'image/webp'],
18
- imageSizes: [16, 32, 48, 64, 96, 128, 256, 384, 512],
19
- deviceSizes: [320, 480, 640, 750, 828, 1080, 1200, 1440, 1920, 2048, 2560],
20
- minimumCacheTTL: 31536000,
21
- dangerouslyAllowSVG: false,
22
- contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
23
- remotePatterns: [
24
- {
25
- protocol: 'https',
26
- hostname: 'pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev',
27
- },
28
- {
29
- protocol: 'https',
30
- hostname: 'e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com',
31
- },
32
- // Add other necessary hostnames, for example, from NEXT_PUBLIC_URL if it's different
33
- // and used for images. This example assumes NEXT_PUBLIC_R2_BASE_URL's hostname is the one above.
34
- // If NEXT_PUBLIC_URL is also an image source and has a different hostname:
35
- ...(process.env.NEXT_PUBLIC_URL
36
- ? [
37
- {
38
- protocol: /** @type {'http' | 'https'} */ (new URL(process.env.NEXT_PUBLIC_URL).protocol.slice(0, -1)),
39
- hostname: new URL(process.env.NEXT_PUBLIC_URL).hostname,
40
- },
41
- ]
42
- : []),
43
- ],
44
- },
45
- experimental: {
46
- optimizeCss: true,
47
- cssChunking: 'strict',
48
- },
49
- transpilePackages: ['@nextblock-cms/utils', '@nextblock-cms/ui', '@nextblock-cms/editor'],
50
- webpack: (config, { isServer }) => {
51
- if (!isServer) {
52
- config.module.rules.push({
53
- test: /\.svg$/i,
54
- issuer: /\.[jt]sx?$/,
55
- use: ['@svgr/webpack'],
56
- });
57
- // Optimize TipTap bundle separation for client-side
58
- config.optimization = {
59
- ...config.optimization,
60
- splitChunks: {
61
- ...config.optimization.splitChunks,
62
- cacheGroups: {
63
- ...config.optimization.splitChunks?.cacheGroups,
64
- // Create a separate chunk for TipTap and related dependencies
65
- tiptap: {
66
- test: /[\\/]node_modules[\\/](@tiptap|prosemirror)[\\/]/,
67
- name: 'tiptap',
68
- chunks: 'async', // Only include in async chunks (dynamic imports)
69
- priority: 30,
70
- reuseExistingChunk: true,
71
- },
72
- // Separate chunk for TipTap extensions and custom components
73
- tiptapExtensions: {
74
- test: /[\\/](tiptap-extensions|RichTextEditor|MenuBar|MediaLibraryModal)[\\/]/,
75
- name: 'tiptap-extensions',
76
- chunks: 'async',
77
- priority: 25,
78
- reuseExistingChunk: true,
79
- },
80
- },
81
- },
82
- };
83
- }
84
- return config;
85
- },
86
- turbopack: {
87
- // Turbopack-specific options can be placed here if needed in the future
88
- },
89
- compiler: {
90
- removeConsole: process.env.NODE_ENV === 'production',
91
- }
92
- };
93
-
94
- const plugins = [
95
- // Add more Next.js plugins to this list if needed.
96
- withNx,
97
- ];
98
-
99
- module.exports = composePlugins(...plugins)(nextConfig);
1
+ //@ts-check
2
+
3
+ /**
4
+ * @typedef {{ protocol?: 'http' | 'https'; hostname: string; port?: string; pathname?: string }} RemotePattern
5
+ */
6
+
7
+ /** @type {import('next').NextConfig} */
8
+ const nextConfig = {
9
+ env: {
10
+ NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
11
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
12
+ },
13
+ images: {
14
+ formats: ['image/avif', 'image/webp'],
15
+ imageSizes: [16, 32, 48, 64, 96, 128, 256, 384, 512],
16
+ deviceSizes: [320, 480, 640, 750, 828, 1080, 1200, 1440, 1920, 2048, 2560],
17
+ qualities: [60, 75],
18
+ minimumCacheTTL: 31_536_000,
19
+ dangerouslyAllowSVG: false,
20
+ contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
21
+ remotePatterns: getRemotePatterns(),
22
+ },
23
+ compiler: {
24
+ removeConsole: process.env.NODE_ENV === 'production',
25
+ },
26
+ transpilePackages: [
27
+ '@nextblock-cms/utils',
28
+ '@nextblock-cms/ui',
29
+ '@nextblock-cms/editor',
30
+ ],
31
+ };
32
+
33
+ module.exports = nextConfig;
34
+
35
+ function getRemotePatterns() {
36
+ /** @type {RemotePattern[]} */
37
+ const patterns = [
38
+ {
39
+ protocol: 'https',
40
+ hostname: 'pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev',
41
+ pathname: '/**',
42
+ },
43
+ {
44
+ protocol: 'https',
45
+ hostname: 'e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com',
46
+ pathname: '/**',
47
+ },
48
+ ];
49
+
50
+ if (process.env.NEXT_PUBLIC_URL) {
51
+ try {
52
+ const parsed = new URL(process.env.NEXT_PUBLIC_URL);
53
+ if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
54
+ patterns.push({
55
+ protocol: parsed.protocol === 'https:' ? 'https' : 'http',
56
+ hostname: parsed.hostname,
57
+ pathname: '/**',
58
+ });
59
+ }
60
+ } catch {
61
+ // ignore malformed value
62
+ }
63
+ }
64
+
65
+ return patterns;
66
+ }
@@ -1,6 +1,6 @@
1
1
  module.exports = {
2
2
  plugins: {
3
- tailwindcss: {},
3
+ '@tailwindcss/postcss': {},
4
4
  autoprefixer: {},
5
5
  },
6
6
  };
@@ -1,206 +1,230 @@
1
- // middleware.ts
2
- import { createServerClient, type CookieOptions } from '@supabase/ssr';
3
- import { NextResponse, type NextRequest } from 'next/server';
4
- import type { Database } from "@nextblock-cms/db";
5
-
6
- type Profile = Database["public"]["Tables"]["profiles"]["Row"];
7
- type UserRole = Database["public"]["Enums"]["user_role"];
8
-
9
- const LANGUAGE_COOKIE_KEY = 'NEXT_USER_LOCALE';
10
- const DEFAULT_LOCALE = 'en';
11
- const SUPPORTED_LOCALES = ['en', 'fr']; // Keep this in sync with DB or make dynamic
12
-
13
- const cmsRoutePermissions: Record<string, UserRole[]> = {
14
- '/cms': ['WRITER', 'ADMIN'],
15
- '/cms/admin': ['ADMIN'],
16
- '/cms/users': ['ADMIN'],
17
- '/cms/settings': ['ADMIN'],
18
- };
19
-
20
- function getRequiredRolesForPath(pathname: string): UserRole[] | null {
21
- const sortedPaths = Object.keys(cmsRoutePermissions).sort((a, b) => b.length - a.length);
22
- for (const specificPath of sortedPaths) {
23
- if (pathname === specificPath || pathname.startsWith(specificPath + (specificPath === '/' ? '' : '/'))) {
24
- return cmsRoutePermissions[specificPath];
25
- }
26
- }
27
- return null;
28
- }
29
-
30
- export async function middleware(request: NextRequest) {
31
- const requestHeaders = new Headers(request.headers);
32
- const nonce = crypto.randomUUID();
33
- requestHeaders.set('x-nonce', nonce);
34
-
35
- let response = NextResponse.next({
36
- request: {
37
- headers: requestHeaders,
38
- },
39
- });
40
-
41
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
42
- const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
43
-
44
- if (!supabaseUrl || !supabaseAnonKey) {
45
- throw new Error('Missing required Supabase environment variables');
46
- }
47
-
48
- const supabase = createServerClient(
49
- supabaseUrl,
50
- supabaseAnonKey,
51
- {
52
- cookies: {
53
- get(name: string) { return request.cookies.get(name)?.value; },
54
- set(name: string, value: string, options: CookieOptions) {
55
- request.cookies.set({ name, value, ...options });
56
- response = NextResponse.next({ request: { headers: requestHeaders } });
57
- response.cookies.set({ name, value, ...options });
58
- },
59
- remove(name: string, options: CookieOptions) {
60
- request.cookies.set({ name, value: '', ...options });
61
- response = NextResponse.next({ request: { headers: requestHeaders } });
62
- response.cookies.set({ name, value: '', ...options });
63
- },
64
- },
65
- }
66
- );
67
-
68
- await supabase.auth.getSession();
69
-
70
- const cookieLocale = request.cookies.get(LANGUAGE_COOKIE_KEY)?.value;
71
- let currentLocale = cookieLocale;
72
-
73
- if (!currentLocale || !SUPPORTED_LOCALES.includes(currentLocale)) {
74
- currentLocale = DEFAULT_LOCALE;
75
- }
76
-
77
- requestHeaders.set('X-User-Locale', currentLocale);
78
-
79
- const { data: { user }, error: userError } = await supabase.auth.getUser(); // Use getUser for revalidation
80
- const { pathname } = request.nextUrl;
81
-
82
- // CMS route protection
83
- if (pathname.startsWith('/cms')) { // Ensure this check is broad enough for all CMS paths
84
- if (userError || !user) { // Check for error or no user
85
- return NextResponse.redirect(new URL(`/sign-in?redirect=${pathname}`, request.url));
86
- }
87
-
88
- const requiredRoles = getRequiredRolesForPath(pathname);
89
-
90
- if (requiredRoles && requiredRoles.length > 0) {
91
- const { data: profile, error: profileError } = await supabase
92
- .from('profiles')
93
- .select('role')
94
- .eq('id', user.id)
95
- .single<Pick<Profile, 'role'>>();
96
-
97
- if (profileError || !profile) {
98
- console.error(`Middleware: Profile error for user ${user.id} accessing ${pathname}. Error: ${profileError?.message}. Redirecting to unauthorized.`);
99
- return NextResponse.redirect(new URL('/unauthorized?error=profile_issue', request.url));
100
- }
101
-
102
- const userRole = profile.role as UserRole;
103
- if (!requiredRoles.includes(userRole)) {
104
- console.warn(`Middleware: User ${user.id} (Role: ${userRole}) denied access to ${pathname}. Required: ${requiredRoles.join(' OR ')}. Redirecting to unauthorized.`);
105
- return NextResponse.redirect(new URL(`/unauthorized?path=${pathname}&required=${requiredRoles.join(',')}`, request.url));
106
- }
107
- }
108
- }
109
-
110
- if (response.headers.get('location')) {
111
- return response;
112
- }
113
-
114
- const finalResponse = NextResponse.next({
115
- request: {
116
- headers: requestHeaders,
117
- },
118
- });
119
-
120
- response.cookies.getAll().forEach(cookie => {
121
- finalResponse.cookies.set(cookie.name, cookie.value, cookie);
122
- });
123
-
124
- if (request.cookies.get(LANGUAGE_COOKIE_KEY)?.value !== currentLocale) {
125
- finalResponse.cookies.set(LANGUAGE_COOKIE_KEY, currentLocale, { path: '/', maxAge: 31536000, sameSite: 'lax' });
126
- }
127
-
128
- if (pathname === '/sign-in' || pathname === '/sign-up' || pathname === '/forgot-password') {
129
- finalResponse.headers.set('X-Page-Type', 'auth');
130
- finalResponse.headers.set('X-Prefetch-Priority', 'critical');
131
- } else if (pathname === '/') {
132
- finalResponse.headers.set('X-Page-Type', 'home');
133
- finalResponse.headers.set('X-Prefetch-Priority', 'high');
134
- } else if (pathname === '/blog') {
135
- finalResponse.headers.set('X-Page-Type', 'blog-index');
136
- finalResponse.headers.set('X-Prefetch-Priority', 'high');
137
- } else if (pathname.startsWith('/blog/')) {
138
- finalResponse.headers.set('X-Page-Type', 'blog-post');
139
- finalResponse.headers.set('X-Prefetch-Priority', 'medium');
140
- } else {
141
- const segments = pathname.split('/').filter(Boolean);
142
- if (segments.length === 1 && !pathname.startsWith('/cms')) {
143
- finalResponse.headers.set('X-Page-Type', 'dynamic-page');
144
- finalResponse.headers.set('X-Prefetch-Priority', 'medium');
145
- }
146
- }
147
-
148
- const acceptHeader = request.headers.get('accept');
149
- if (
150
- acceptHeader &&
151
- acceptHeader.includes('text/html') &&
152
- !pathname.startsWith('/api/')
153
- ) {
154
- finalResponse.headers.set('Cache-Control', 'public, max-age=0, must-revalidate');
155
- finalResponse.headers.set('X-BFCache-Applied', 'true');
156
- }
157
-
158
- finalResponse.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
159
- finalResponse.headers.set('X-Frame-Options', 'SAMEORIGIN');
160
- finalResponse.headers.set('X-Content-Type-Options', 'nosniff');
161
- finalResponse.headers.set('Referrer-Policy', 'origin-when-cross-origin');
162
- finalResponse.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
163
- finalResponse.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
164
-
165
- // Only send nonce-based CSP in production. In development, Next.js
166
- // Dev Overlay injects inline scripts without a nonce, which would be blocked.
167
- if (process.env.NODE_ENV === 'production') {
168
- const nonceValue = requestHeaders.get('x-nonce');
169
- if (nonceValue) {
170
- const csp = [
171
- "default-src 'self'",
172
- `script-src 'self' blob: data: 'nonce-${nonceValue}'`,
173
- "style-src 'self' 'unsafe-inline'",
174
- "img-src 'self' data: blob: https://nrh-next-cms.e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com https://pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev",
175
- "font-src 'self'",
176
- "object-src 'none'",
177
- "connect-src 'self' https://ppcppwsfnrptznvbxnsz.supabase.co wss://ppcppwsfnrptznvbxnsz.supabase.co https://nrh-next-cms.e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com https://pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev",
178
- "frame-src 'self' blob: data: https://www.youtube.com",
179
- "form-action 'self'",
180
- "base-uri 'self'",
181
- ].join('; ');
182
-
183
- finalResponse.headers.set('Content-Security-Policy', csp);
184
- }
185
- }
186
-
187
- const responseForLogging = finalResponse.clone();
188
- const cacheStatus = responseForLogging.headers.get('x-vercel-cache') || 'none';
189
-
190
- if (!pathname.startsWith('/api/')) {
191
- console.log(JSON.stringify({
192
- type: 'cache',
193
- status: cacheStatus,
194
- path: pathname,
195
- }));
196
- }
197
-
198
- return finalResponse;
199
- }
200
-
201
- export const config = {
202
- matcher: [
203
- '/((?!_next/static|_next/image|favicon.ico|auth/.*|sign-in|sign-up|forgot-password|unauthorized|api/auth/.*|api/revalidate|api/revalidate-log).*)',
204
- '/cms/:path*',
205
- ],
206
- };
1
+ import { createServerClient, type CookieOptions } from '@supabase/ssr';
2
+ import { NextResponse, type NextRequest } from 'next/server';
3
+ import type { Database } from '@nextblock-cms/db';
4
+
5
+ type Profile = Database['public']['Tables']['profiles']['Row'];
6
+ type UserRole = Database['public']['Enums']['user_role'];
7
+
8
+ const LANGUAGE_COOKIE_KEY = 'NEXT_USER_LOCALE';
9
+ const DEFAULT_LOCALE = 'en';
10
+ const SUPPORTED_LOCALES = ['en', 'fr'];
11
+
12
+ const cmsRoutePermissions: Record<string, UserRole[]> = {
13
+ '/cms': ['WRITER', 'ADMIN'],
14
+ '/cms/admin': ['ADMIN'],
15
+ '/cms/users': ['ADMIN'],
16
+ '/cms/settings': ['ADMIN'],
17
+ };
18
+
19
+ function getRequiredRolesForPath(pathname: string): UserRole[] | null {
20
+ const sortedPaths = Object.keys(cmsRoutePermissions).sort(
21
+ (a, b) => b.length - a.length,
22
+ );
23
+ for (const specificPath of sortedPaths) {
24
+ if (
25
+ pathname === specificPath ||
26
+ pathname.startsWith(specificPath + (specificPath === '/' ? '' : '/'))
27
+ ) {
28
+ return cmsRoutePermissions[specificPath];
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+
34
+ export default async function proxy(request: NextRequest) {
35
+ const requestHeaders = new Headers(request.headers);
36
+ const nonce = crypto.randomUUID();
37
+ requestHeaders.set('x-nonce', nonce);
38
+
39
+ let response = NextResponse.next({
40
+ request: {
41
+ headers: requestHeaders,
42
+ },
43
+ });
44
+
45
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
46
+ const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
47
+
48
+ if (!supabaseUrl || !supabaseAnonKey) {
49
+ throw new Error('Missing required Supabase environment variables');
50
+ }
51
+
52
+ const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
53
+ cookies: {
54
+ get(name: string) {
55
+ return request.cookies.get(name)?.value;
56
+ },
57
+ set(name: string, value: string, options: CookieOptions) {
58
+ request.cookies.set({ name, value, ...options });
59
+ response = NextResponse.next({ request: { headers: requestHeaders } });
60
+ response.cookies.set({ name, value, ...options });
61
+ },
62
+ remove(name: string, options: CookieOptions) {
63
+ request.cookies.set({ name, value: '', ...options });
64
+ response = NextResponse.next({ request: { headers: requestHeaders } });
65
+ response.cookies.set({ name, value: '', ...options });
66
+ },
67
+ },
68
+ });
69
+
70
+ await supabase.auth.getSession();
71
+
72
+ const cookieLocale = request.cookies.get(LANGUAGE_COOKIE_KEY)?.value;
73
+ let currentLocale = cookieLocale;
74
+
75
+ if (!currentLocale || !SUPPORTED_LOCALES.includes(currentLocale)) {
76
+ currentLocale = DEFAULT_LOCALE;
77
+ }
78
+
79
+ requestHeaders.set('X-User-Locale', currentLocale);
80
+
81
+ const {
82
+ data: { user },
83
+ error: userError,
84
+ } = await supabase.auth.getUser();
85
+ const { pathname } = request.nextUrl;
86
+
87
+ if (pathname.startsWith('/cms')) {
88
+ if (userError || !user) {
89
+ return NextResponse.redirect(
90
+ new URL(`/sign-in?redirect=${pathname}`, request.url),
91
+ );
92
+ }
93
+
94
+ const requiredRoles = getRequiredRolesForPath(pathname);
95
+
96
+ if (requiredRoles && requiredRoles.length > 0) {
97
+ const {
98
+ data: profile,
99
+ error: profileError,
100
+ } = await supabase
101
+ .from('profiles')
102
+ .select('role')
103
+ .eq('id', user.id)
104
+ .single<Pick<Profile, 'role'>>();
105
+
106
+ if (profileError || !profile) {
107
+ console.error(
108
+ `Proxy: Profile error for user ${user.id} accessing ${pathname}. Error: ${profileError?.message}. Redirecting to unauthorized.`,
109
+ );
110
+ return NextResponse.redirect(
111
+ new URL('/unauthorized?error=profile_issue', request.url),
112
+ );
113
+ }
114
+
115
+ const userRole = profile.role as UserRole;
116
+ if (!requiredRoles.includes(userRole)) {
117
+ console.warn(
118
+ `Proxy: User ${user.id} (Role: ${userRole}) denied access to ${pathname}. Required: ${requiredRoles.join(' OR ')}. Redirecting to unauthorized.`,
119
+ );
120
+ return NextResponse.redirect(
121
+ new URL(
122
+ `/unauthorized?path=${pathname}&required=${requiredRoles.join(',')}`,
123
+ request.url,
124
+ ),
125
+ );
126
+ }
127
+ }
128
+ }
129
+
130
+ if (response.headers.get('location')) {
131
+ return response;
132
+ }
133
+
134
+ const finalResponse = NextResponse.next({
135
+ request: {
136
+ headers: requestHeaders,
137
+ },
138
+ });
139
+
140
+ response.cookies.getAll().forEach((cookie) => {
141
+ finalResponse.cookies.set(cookie.name, cookie.value, cookie);
142
+ });
143
+
144
+ if (request.cookies.get(LANGUAGE_COOKIE_KEY)?.value !== currentLocale) {
145
+ finalResponse.cookies.set(LANGUAGE_COOKIE_KEY, currentLocale, {
146
+ path: '/',
147
+ maxAge: 31_536_000,
148
+ sameSite: 'lax',
149
+ });
150
+ }
151
+
152
+ if (
153
+ pathname === '/sign-in' ||
154
+ pathname === '/sign-up' ||
155
+ pathname === '/forgot-password'
156
+ ) {
157
+ finalResponse.headers.set('X-Page-Type', 'auth');
158
+ finalResponse.headers.set('X-Prefetch-Priority', 'critical');
159
+ } else if (pathname === '/') {
160
+ finalResponse.headers.set('X-Page-Type', 'home');
161
+ finalResponse.headers.set('X-Prefetch-Priority', 'high');
162
+ } else if (pathname === '/blog') {
163
+ finalResponse.headers.set('X-Page-Type', 'blog-index');
164
+ finalResponse.headers.set('X-Prefetch-Priority', 'high');
165
+ } else if (pathname.startsWith('/blog/')) {
166
+ finalResponse.headers.set('X-Page-Type', 'blog-post');
167
+ finalResponse.headers.set('X-Prefetch-Priority', 'medium');
168
+ } else {
169
+ const segments = pathname.split('/').filter(Boolean);
170
+ if (segments.length === 1 && !pathname.startsWith('/cms')) {
171
+ finalResponse.headers.set('X-Page-Type', 'dynamic-page');
172
+ finalResponse.headers.set('X-Prefetch-Priority', 'medium');
173
+ }
174
+ }
175
+
176
+ const acceptHeader = request.headers.get('accept');
177
+ if (acceptHeader && acceptHeader.includes('text/html') && !pathname.startsWith('/api/')) {
178
+ finalResponse.headers.set('Cache-Control', 'public, max-age=0, must-revalidate');
179
+ finalResponse.headers.set('X-BFCache-Applied', 'true');
180
+ }
181
+
182
+ finalResponse.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
183
+ finalResponse.headers.set('X-Frame-Options', 'SAMEORIGIN');
184
+ finalResponse.headers.set('X-Content-Type-Options', 'nosniff');
185
+ finalResponse.headers.set('Referrer-Policy', 'origin-when-cross-origin');
186
+ finalResponse.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
187
+ finalResponse.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
188
+
189
+ if (process.env.NODE_ENV === 'production') {
190
+ const nonceValue = requestHeaders.get('x-nonce');
191
+ if (nonceValue) {
192
+ const csp = [
193
+ "default-src 'self'",
194
+ `script-src 'self' blob: data: 'nonce-${nonceValue}'`,
195
+ "style-src 'self' 'unsafe-inline'",
196
+ "img-src 'self' data: blob: https://nrh-next-cms.e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com https://pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev",
197
+ "font-src 'self'",
198
+ "object-src 'none'",
199
+ "connect-src 'self' https://ppcppwsfnrptznvbxnsz.supabase.co wss://ppcppwsfnrptznvbxnsz.supabase.co https://nrh-next-cms.e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com https://pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev",
200
+ "frame-src 'self' blob: data: https://www.youtube.com",
201
+ "form-action 'self'",
202
+ "base-uri 'self'",
203
+ ].join('; ');
204
+
205
+ finalResponse.headers.set('Content-Security-Policy', csp);
206
+ }
207
+ }
208
+
209
+ const responseForLogging = finalResponse.clone();
210
+ const cacheStatus = responseForLogging.headers.get('x-vercel-cache') || 'none';
211
+
212
+ if (!pathname.startsWith('/api/')) {
213
+ console.log(
214
+ JSON.stringify({
215
+ type: 'cache',
216
+ status: cacheStatus,
217
+ path: pathname,
218
+ }),
219
+ );
220
+ }
221
+
222
+ return finalResponse;
223
+ }
224
+
225
+ export const config = {
226
+ matcher: [
227
+ '/((?!_next/static|_next/image|favicon.ico|auth/.*|sign-in|sign-up|forgot-password|unauthorized|api/auth/.*|api/revalidate|api/revalidate-log).*)',
228
+ '/cms/:path*',
229
+ ],
230
+ };
@@ -0,0 +1,26 @@
1
+ const { existsSync } = require('fs');
2
+ const { join } = require('path');
3
+
4
+ /** @type {import('tailwindcss').Config} */
5
+ module.exports = {
6
+ presets: [require('../../tailwind.config.js')],
7
+ content: (() => {
8
+ const projectGlobs = [
9
+ join(
10
+ __dirname,
11
+ '{src,pages,components,app,lib}/**/*!(*.stories|*.spec).{ts,tsx,js,jsx,md,mdx,html}'
12
+ ),
13
+ ];
14
+ const libsDir = join(__dirname, '../../libs');
15
+ if (existsSync(libsDir)) {
16
+ projectGlobs.push(
17
+ join(libsDir, '**/*.{ts,tsx,js,jsx,md,mdx,html}')
18
+ );
19
+ }
20
+ return projectGlobs;
21
+ })(),
22
+ theme: {
23
+ extend: {},
24
+ },
25
+ plugins: [],
26
+ };
@@ -0,0 +1 @@
1
+ module.exports = {};
@@ -1,19 +0,0 @@
1
- // File: apps/nextblock/tailwind.config.ts
2
- const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
3
- const { join } = require('path');
4
-
5
- /** @type {import('tailwindcss').Config} */
6
- module.exports = {
7
- presets: [require('../../tailwind.config.ts')],
8
- content: [
9
- join(
10
- __dirname,
11
- '{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}'
12
- ),
13
- ...createGlobPatternsForDependencies(__dirname),
14
- ],
15
- theme: {
16
- extend: {},
17
- },
18
- plugins: [],
19
- };