generator-bitloops 0.3.27 → 0.3.29

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generator-bitloops",
3
- "version": "0.3.27",
3
+ "version": "0.3.29",
4
4
  "description": "Next.js with TypeScript, Tailwind, Storybook and Cypress generator by Bitloops",
5
5
  "license": "MIT",
6
6
  "author": "Bitloops S.A.",
package/setup/index.js CHANGED
@@ -10,8 +10,11 @@ const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = path.dirname(__filename);
11
11
 
12
12
  const DOT = '.';
13
+ const STORYBOOK_FOLDER = '.storybook';
13
14
  const PLATFORM_NEXT_FOLDER = 'platform-next';
14
15
  const PLATFORM_NEXT_SRC_FOLDER = `${PLATFORM_NEXT_FOLDER}/src`;
16
+ const PLATFORM_VITE_FOLDER = 'platform-vite';
17
+ const PLATFORM_VITE_SRC_FOLDER = `${PLATFORM_VITE_FOLDER}/src`;
15
18
 
16
19
  function isKebabCase(str) {
17
20
  // Check if the string is empty
@@ -124,6 +127,54 @@ export default class extends Generator {
124
127
  default: false,
125
128
  });
126
129
 
130
+ this.option('redux', {
131
+ type: Boolean,
132
+ description: 'Add Redux Toolkit and React Redux for state management',
133
+ default: false,
134
+ });
135
+
136
+ this.option('vitest', {
137
+ type: Boolean,
138
+ description: 'Add Vitest testing framework with coverage and UI',
139
+ default: false,
140
+ });
141
+
142
+ this.option('webVitals', {
143
+ type: Boolean,
144
+ description: 'Add web-vitals for performance monitoring',
145
+ default: false,
146
+ });
147
+
148
+ this.option('zod', {
149
+ type: Boolean,
150
+ description: 'Add Zod for schema validation',
151
+ default: false,
152
+ });
153
+
154
+ this.option('bundleAnalyzer', {
155
+ type: Boolean,
156
+ description: 'Add @next/bundle-analyzer for bundle analysis',
157
+ default: false,
158
+ });
159
+
160
+ this.option('reactIcons', {
161
+ type: Boolean,
162
+ description: 'Add react-icons library',
163
+ default: false,
164
+ });
165
+
166
+ this.option('msw', {
167
+ type: Boolean,
168
+ description: 'Add Mock Service Worker (MSW) for API mocking',
169
+ default: false,
170
+ });
171
+
172
+ this.option('reactCompiler', {
173
+ type: Boolean,
174
+ description: 'Add babel-plugin-react-compiler',
175
+ default: false,
176
+ });
177
+
127
178
  this.installNextJS = async function () {
128
179
  // Clone Next.js template with Tailwind if specified, using the project name
129
180
  const createNextAppCommand = ['-y', 'create-next-app@latest'];
@@ -212,11 +263,10 @@ export default class extends Generator {
212
263
  );
213
264
  this.log('Storybook installed!');
214
265
  // Verifies the correct nextjs-vite framework is used
215
- spawnSync(
216
- 'pnpm',
217
- ['add', '-D', '@storybook/nextjs-vite@^10'],
218
- { stdio: 'inherit', cwd: this.destinationRoot() },
219
- );
266
+ spawnSync('pnpm', ['add', '-D', '@storybook/nextjs-vite@^10'], {
267
+ stdio: 'inherit',
268
+ cwd: this.destinationRoot(),
269
+ });
220
270
  this.log('@storybook/nextjs-vite installed!');
221
271
  }
222
272
  };
@@ -250,11 +300,10 @@ export default class extends Generator {
250
300
  // Conditionally add i18n packages
251
301
  if (this.options.i18n) {
252
302
  this.log('Installing i18n packages...');
253
- spawnSync(
254
- 'pnpm',
255
- ['add', 'i18next', 'i18next-icu', 'react-i18next'],
256
- { stdio: 'inherit', cwd: this.destinationRoot() },
257
- );
303
+ spawnSync('pnpm', ['add', 'i18next', 'i18next-icu', 'react-i18next'], {
304
+ stdio: 'inherit',
305
+ cwd: this.destinationRoot(),
306
+ });
258
307
  this.log('i18n packages installed!');
259
308
  }
260
309
  };
@@ -263,12 +312,139 @@ export default class extends Generator {
263
312
  // Conditionally add Base UI
264
313
  if (this.options.baseUi) {
265
314
  this.log('Installing Base UI...');
315
+ spawnSync('pnpm', ['add', '@base-ui/react@^1.1.0'], {
316
+ stdio: 'inherit',
317
+ cwd: this.destinationRoot(),
318
+ });
319
+ this.log('Base UI installed!');
320
+ }
321
+ };
322
+
323
+ this.installRedux = function () {
324
+ // Conditionally add Redux Toolkit and React Redux
325
+ if (this.options.redux) {
326
+ this.log('Installing Redux Toolkit and React Redux...');
327
+ spawnSync('pnpm', ['add', '@reduxjs/toolkit', 'react-redux'], {
328
+ stdio: 'inherit',
329
+ cwd: this.destinationRoot(),
330
+ });
331
+ this.log('Redux Toolkit and React Redux installed!');
332
+ }
333
+ };
334
+
335
+ this.installVitest = function () {
336
+ // Conditionally add Vitest and related testing packages
337
+ if (this.options.vitest) {
338
+ this.log('Installing Vitest and testing packages...');
266
339
  spawnSync(
267
340
  'pnpm',
268
- ['add', '@base-ui/react@^1.1.0'],
341
+ [
342
+ 'add',
343
+ '-D',
344
+ 'vitest',
345
+ '@vitest/ui',
346
+ '@vitest/coverage-v8',
347
+ '@vitest/browser-playwright',
348
+ '@testing-library/react',
349
+ '@testing-library/jest-dom',
350
+ '@testing-library/user-event',
351
+ '@vitejs/plugin-react',
352
+ 'vite',
353
+ 'jsdom',
354
+ 'playwright',
355
+ ],
269
356
  { stdio: 'inherit', cwd: this.destinationRoot() },
270
357
  );
271
- this.log('Base UI installed!');
358
+ this.log('Vitest and testing packages installed!');
359
+ }
360
+ };
361
+
362
+ this.installWebVitals = function () {
363
+ // Conditionally add web-vitals
364
+ if (this.options.webVitals) {
365
+ this.log('Installing web-vitals...');
366
+ spawnSync('pnpm', ['add', 'web-vitals'], {
367
+ stdio: 'inherit',
368
+ cwd: this.destinationRoot(),
369
+ });
370
+ this.log('web-vitals installed!');
371
+ }
372
+ };
373
+
374
+ this.installZod = function () {
375
+ // Conditionally add Zod
376
+ if (this.options.zod) {
377
+ this.log('Installing Zod...');
378
+ spawnSync('pnpm', ['add', 'zod'], {
379
+ stdio: 'inherit',
380
+ cwd: this.destinationRoot(),
381
+ });
382
+ this.log('Zod installed!');
383
+ }
384
+ };
385
+
386
+ this.installBundleAnalyzer = function () {
387
+ // Conditionally add @next/bundle-analyzer
388
+ if (this.options.bundleAnalyzer) {
389
+ this.log('Installing @next/bundle-analyzer...');
390
+ spawnSync('pnpm', ['add', '-D', '@next/bundle-analyzer'], {
391
+ stdio: 'inherit',
392
+ cwd: this.destinationRoot(),
393
+ });
394
+ this.log('@next/bundle-analyzer installed!');
395
+ }
396
+ };
397
+
398
+ this.installReactIcons = function () {
399
+ // Conditionally add react-icons
400
+ if (this.options.reactIcons) {
401
+ this.log('Installing react-icons...');
402
+ spawnSync('pnpm', ['add', 'react-icons'], {
403
+ stdio: 'inherit',
404
+ cwd: this.destinationRoot(),
405
+ });
406
+ this.log('react-icons installed!');
407
+ }
408
+ };
409
+
410
+ this.installMsw = function () {
411
+ // Conditionally add Mock Service Worker
412
+ if (this.options.msw) {
413
+ this.log('Installing Mock Service Worker (MSW)...');
414
+ spawnSync('pnpm', ['add', '-D', 'msw'], {
415
+ stdio: 'inherit',
416
+ cwd: this.destinationRoot(),
417
+ });
418
+ // Initialize MSW
419
+ spawnSync('npx', ['msw', 'init', 'public/', '--save'], {
420
+ stdio: 'inherit',
421
+ cwd: this.destinationRoot(),
422
+ });
423
+ this.log('MSW installed!');
424
+ }
425
+ };
426
+
427
+ this.installReactCompiler = function () {
428
+ // Conditionally add babel-plugin-react-compiler
429
+ if (this.options.reactCompiler) {
430
+ this.log('Installing babel-plugin-react-compiler...');
431
+ spawnSync('pnpm', ['add', '-D', 'babel-plugin-react-compiler'], {
432
+ stdio: 'inherit',
433
+ cwd: this.destinationRoot(),
434
+ });
435
+ this.log('babel-plugin-react-compiler installed!');
436
+ }
437
+ };
438
+
439
+ this.installIntlMessageFormat = function () {
440
+ // Add intl-messageformat when i18n is enabled
441
+ if (this.options.i18n) {
442
+ this.log('Installing intl-messageformat...');
443
+ spawnSync('pnpm', ['add', 'intl-messageformat'], {
444
+ stdio: 'inherit',
445
+ cwd: this.destinationRoot(),
446
+ });
447
+ this.log('intl-messageformat installed!');
272
448
  }
273
449
  };
274
450
 
@@ -277,26 +453,41 @@ export default class extends Generator {
277
453
  if (this.options.primitives) {
278
454
  this.log('Installing Primitives...');
279
455
 
280
- const platformNextIndexPath = `${PLATFORM_NEXT_SRC_FOLDER}/index.ts`;
281
- deleteFileIfExists(this.destinationPath(platformNextIndexPath));
282
- this.fs.copyTpl(
283
- this.templatePath(platformNextIndexPath),
284
- this.destinationPath(platformNextIndexPath),
285
- );
286
-
287
- const platformNextImgPath = `${PLATFORM_NEXT_SRC_FOLDER}/Img.tsx`;
288
- deleteFileIfExists(this.destinationPath(platformNextImgPath));
289
- this.fs.copyTpl(
290
- this.templatePath(platformNextImgPath),
291
- this.destinationPath(platformNextImgPath),
292
- );
456
+ // Platform Next files
457
+ const platformNextFiles = [
458
+ `${PLATFORM_NEXT_SRC_FOLDER}/index.ts`,
459
+ `${PLATFORM_NEXT_SRC_FOLDER}/Img.tsx`,
460
+ `${PLATFORM_NEXT_SRC_FOLDER}/Link.tsx`,
461
+ `${PLATFORM_NEXT_SRC_FOLDER}/setup.ts`,
462
+ `${PLATFORM_NEXT_SRC_FOLDER}/router/index.ts`,
463
+ `${PLATFORM_NEXT_SRC_FOLDER}/router/useNextRouter.ts`,
464
+ ];
465
+
466
+ platformNextFiles.forEach((filePath) => {
467
+ deleteFileIfExists(this.destinationPath(filePath));
468
+ this.fs.copyTpl(
469
+ this.templatePath(filePath),
470
+ this.destinationPath(filePath),
471
+ );
472
+ });
293
473
 
294
- const platformNextTypesPath = `${PLATFORM_NEXT_SRC_FOLDER}/types.ts`;
295
- deleteFileIfExists(this.destinationPath(platformNextTypesPath));
296
- this.fs.copyTpl(
297
- this.templatePath(platformNextTypesPath),
298
- this.destinationPath(platformNextTypesPath),
299
- );
474
+ // Platform Vite files
475
+ const platformViteFiles = [
476
+ `${PLATFORM_VITE_SRC_FOLDER}/index.ts`,
477
+ `${PLATFORM_VITE_SRC_FOLDER}/Img.tsx`,
478
+ `${PLATFORM_VITE_SRC_FOLDER}/Link.tsx`,
479
+ `${PLATFORM_VITE_SRC_FOLDER}/setup.ts`,
480
+ `${PLATFORM_VITE_SRC_FOLDER}/router/index.ts`,
481
+ `${PLATFORM_VITE_SRC_FOLDER}/router/useViteRouter.ts`,
482
+ ];
483
+
484
+ platformViteFiles.forEach((filePath) => {
485
+ deleteFileIfExists(this.destinationPath(filePath));
486
+ this.fs.copyTpl(
487
+ this.templatePath(filePath),
488
+ this.destinationPath(filePath),
489
+ );
490
+ });
300
491
 
301
492
  this.log('Primitives installed!');
302
493
  }
@@ -306,14 +497,25 @@ export default class extends Generator {
306
497
  // Conditionally initialize Storybook
307
498
  if (this.options.storybook) {
308
499
  this.log('Making Storybook changes...');
309
- if (this.options.tailwind) {
310
- deleteFileIfExists(this.destinationPath('.storybook/preview.ts'));
311
- this.log('Setting up Tailwind CSS with Storybook...');
500
+
501
+ // Copy .storybook template files
502
+ const storybookFiles = [
503
+ `${STORYBOOK_FOLDER}/main.ts`,
504
+ `${STORYBOOK_FOLDER}/preview.tsx`,
505
+ `${STORYBOOK_FOLDER}/vitest.setup.ts`,
506
+ ];
507
+
508
+ // Delete .storybook/preview.ts if it exists (generated by storybook init)
509
+ deleteFileIfExists(this.destinationPath(`${STORYBOOK_FOLDER}/preview.ts`));
510
+
511
+ storybookFiles.forEach((filePath) => {
512
+ deleteFileIfExists(this.destinationPath(filePath));
312
513
  this.fs.copyTpl(
313
- this.templatePath('storybook.preview.ts'),
314
- this.destinationPath('.storybook/preview.ts'),
514
+ this.templatePath(filePath),
515
+ this.destinationPath(filePath),
315
516
  );
316
- }
517
+ });
518
+
317
519
  this.log('Removing default Storybook stories...');
318
520
  try {
319
521
  fs.rmSync(this.destinationPath('src/stories'), {
@@ -402,6 +604,39 @@ export default class extends Generator {
402
604
  }
403
605
  };
404
606
 
607
+ this.patchPackageJsonScripts = async function () {
608
+ this.log('Patching package.json scripts...');
609
+ const packageJsonPath = this.destinationPath('package.json');
610
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
611
+
612
+ // Add vitest scripts if vitest is enabled
613
+ if (this.options.vitest) {
614
+ packageJson.scripts.test = 'vitest';
615
+ packageJson.scripts['test:ui'] = 'vitest --ui';
616
+ packageJson.scripts['test:coverage'] = 'vitest run --coverage';
617
+ }
618
+
619
+ // Add type-check script if typescript is enabled
620
+ if (this.options.typescript) {
621
+ packageJson.scripts['type-check'] = 'tsc --noEmit';
622
+ }
623
+
624
+ // Add analyze script if bundleAnalyzer is enabled
625
+ if (this.options.bundleAnalyzer) {
626
+ packageJson.scripts.analyze = 'ANALYZE=true next build';
627
+ }
628
+
629
+ // Add msw configuration if msw is enabled
630
+ if (this.options.msw) {
631
+ packageJson.msw = {
632
+ workerDirectory: ['public'],
633
+ };
634
+ }
635
+
636
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
637
+ this.log('package.json scripts patched!');
638
+ };
639
+
405
640
  this.commitChanges = async function () {
406
641
  this.log('Committing changes to git...');
407
642
  await new Promise((resolve) => {
@@ -448,10 +683,20 @@ export default class extends Generator {
448
683
  await this.installNextJS();
449
684
  this.installCypress();
450
685
  this.installI18n();
686
+ this.installIntlMessageFormat();
451
687
  this.installBaseUi();
688
+ this.installRedux();
689
+ this.installVitest();
690
+ this.installWebVitals();
691
+ this.installZod();
692
+ this.installBundleAnalyzer();
693
+ this.installReactIcons();
694
+ this.installMsw();
695
+ this.installReactCompiler();
452
696
  this.installPrimitives();
453
697
  this.installStorybook();
454
698
  await this.patchFiles();
699
+ await this.patchPackageJsonScripts();
455
700
  if (this.options.git) {
456
701
  await this.commitChanges();
457
702
  }
@@ -0,0 +1,32 @@
1
+ import type { StorybookConfig } from '@storybook/nextjs-vite';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ const config: StorybookConfig = {
8
+ "stories": [
9
+ "../src/**/*.mdx",
10
+ "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
11
+ ],
12
+ "addons": [
13
+ "@chromatic-com/storybook",
14
+ "@storybook/addon-vitest",
15
+ "@storybook/addon-a11y",
16
+ "@storybook/addon-docs"
17
+ ],
18
+ "framework": "@storybook/nextjs-vite",
19
+ "staticDirs": [
20
+ "../public"
21
+ ],
22
+ async viteFinal(config) {
23
+ if (config.resolve) {
24
+ config.resolve.alias = {
25
+ ...config.resolve.alias,
26
+ '@/primitives': path.resolve(__dirname, '../platform-vite/src'),
27
+ };
28
+ }
29
+ return config;
30
+ }
31
+ };
32
+ export default config;
@@ -0,0 +1,58 @@
1
+ import React from "react";
2
+ import type { Preview } from "@storybook/nextjs-vite";
3
+ import {
4
+ fontConfig,
5
+ generateGoogleFontsUrl,
6
+ } from "../src/lib/fonts/fonts.config";
7
+ import "../platform-vite/src/setup";
8
+ import "../src/app/globals.css";
9
+ import "../src/i18n";
10
+ import { worker } from "../src/mocks/browser";
11
+ import { Provider } from "react-redux";
12
+ import { store } from "../src/store";
13
+
14
+ await worker.start({ onUnhandledRequest: "bypass" });
15
+
16
+ // Inject Google Fonts stylesheet and CSS variables
17
+ if (typeof document !== "undefined") {
18
+ const link = document.createElement("link");
19
+ link.rel = "stylesheet";
20
+ link.href = generateGoogleFontsUrl();
21
+ document.head.appendChild(link);
22
+
23
+ // Set CSS variables
24
+ const style = document.createElement("style");
25
+ style.textContent = `
26
+ :root {
27
+ ${fontConfig.montserrat.variable}: '${fontConfig.montserrat.family}', system-ui, sans-serif;
28
+ ${fontConfig.roboto.variable}: '${fontConfig.roboto.family}', system-ui, sans-serif;
29
+ }
30
+ `;
31
+ document.head.appendChild(style);
32
+ }
33
+
34
+ const preview: Preview = {
35
+ parameters: {
36
+ controls: {
37
+ matchers: {
38
+ color: /(background|color)$/i,
39
+ date: /Date$/i,
40
+ },
41
+ },
42
+ a11y: {
43
+ // 'todo' - show a11y violations in the test UI only
44
+ // 'error' - fail CI on a11y violations
45
+ // 'off' - skip a11y checks entirely
46
+ test: "todo",
47
+ },
48
+ },
49
+ decorators: [
50
+ (Story) => (
51
+ <Provider store={store}>
52
+ <Story />
53
+ </Provider>
54
+ ),
55
+ ],
56
+ };
57
+
58
+ export default preview;
@@ -0,0 +1,7 @@
1
+ import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
2
+ import { setProjectAnnotations } from '@storybook/nextjs-vite';
3
+ import * as projectAnnotations from './preview';
4
+
5
+ // This is an important step to apply the right configuration when testing your stories.
6
+ // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
7
+ setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
@@ -1,7 +1,8 @@
1
- 'use client';
2
- import NextImage from 'next/image';
3
- import type { ImgProps } from './types';
1
+ "use client";
2
+ import NextImage from "next/image";
3
+ import type { ImgProps } from "../../src/lib/types/primitives.types";
4
+
4
5
  export function Img(props: ImgProps) {
5
- const { responsive, ...rest } = props;
6
+ const { ...rest } = props;
6
7
  return <NextImage {...rest} />;
7
- }
8
+ }
@@ -0,0 +1,6 @@
1
+ "use client";
2
+ import NextLink from "next/link";
3
+ import type { LinkProps } from "../../src/lib/types/primitives.types";
4
+ export function Link(props: LinkProps) {
5
+ return <NextLink {...props} />;
6
+ }
@@ -1,2 +1,3 @@
1
- export * from './types';
2
- export * from './Img';
1
+ export * from "../../src/lib/types/primitives.types";
2
+ export * from "./Img";
3
+ export * from "./Link";
@@ -0,0 +1 @@
1
+ export { useNextRouter } from './useNextRouter';
@@ -0,0 +1,14 @@
1
+ "use client";
2
+
3
+ import { useRouter as useNextNavigationRouter } from 'next/navigation';
4
+ import type { Router } from '@/lib/router/types';
5
+
6
+ export function useNextRouter(): Router {
7
+ const nextRouter = useNextNavigationRouter();
8
+
9
+ return {
10
+ push: (url: string) => nextRouter.push(url),
11
+ replace: (url: string) => nextRouter.replace(url),
12
+ back: () => nextRouter.back(),
13
+ };
14
+ }
@@ -0,0 +1,7 @@
1
+ "use client";
2
+
3
+ import { setRouterImplementation } from '@/lib/router/useRouter';
4
+ import { useNextRouter } from './router/useNextRouter';
5
+
6
+ // Set up Next.js router implementation
7
+ setRouterImplementation(useNextRouter);
@@ -0,0 +1,18 @@
1
+ /* eslint-disable @next/next/no-img-element */
2
+ import type { ImgProps } from "../../src/lib/types/primitives.types";
3
+
4
+ export function Img(props: ImgProps) {
5
+ const { src, alt, className, style, loading, decoding } =
6
+ props;
7
+
8
+ return (
9
+ <img
10
+ src={src}
11
+ alt={alt}
12
+ className={className}
13
+ style={style}
14
+ loading={loading}
15
+ decoding={decoding}
16
+ />
17
+ );
18
+ }
@@ -0,0 +1,19 @@
1
+ import type { LinkProps } from "../../src/lib/types/primitives.types";
2
+
3
+ export function Link(props: LinkProps) {
4
+ const { href, children, target, rel, className, style, onClick } = props;
5
+
6
+ return (
7
+ <a
8
+ href={href}
9
+ target={target}
10
+ rel={rel}
11
+ className={className}
12
+ style={style}
13
+ onClick={onClick}
14
+ aria-label={props["aria-label"]}
15
+ >
16
+ {children}
17
+ </a>
18
+ );
19
+ }
@@ -0,0 +1,3 @@
1
+ export * from "../../src/lib/types/primitives.types";
2
+ export * from "./Img";
3
+ export * from "./Link";
@@ -0,0 +1 @@
1
+ export { useViteRouter } from "./useViteRouter";
@@ -0,0 +1,17 @@
1
+ import type { Router } from "@/lib/router/types";
2
+
3
+ export function useViteRouter(): Router {
4
+ return {
5
+ push: (url: string) => {
6
+ console.log("[Storybook Router] Push to:", url);
7
+ // In Storybook, we don't actually navigate
8
+ // This could be enhanced with Storybook actions addon
9
+ },
10
+ replace: (url: string) => {
11
+ console.log("[Storybook Router] Replace with:", url);
12
+ },
13
+ back: () => {
14
+ console.log("[Storybook Router] Go back");
15
+ },
16
+ };
17
+ }
@@ -0,0 +1,5 @@
1
+ import { setRouterImplementation } from "@/lib/router/useRouter";
2
+ import { useViteRouter } from "./router/useViteRouter";
3
+
4
+ // Set up Vite/Storybook router implementation
5
+ setRouterImplementation(useViteRouter);
@@ -1,28 +0,0 @@
1
- export type ImgProps = {
2
- src: string; alt: string;
3
- width?: number; height?: number; fill?: boolean;
4
- sizes?: string; priority?: boolean; quality?: number;
5
- className?: string; style?: React.CSSProperties;
6
- loading?: 'eager'|'lazy'; decoding?: 'auto'|'sync'|'async';
7
- responsive?: { sources: Array<{ media: string; srcSet: string }> }; // optional <picture>
8
- };
9
-
10
- export type LinkProps = {
11
- href: string; children: React.ReactNode;
12
- prefetch?: boolean; replace?: boolean; scroll?: boolean;
13
- target?: React.HTMLAttributeAnchorTarget; rel?: string;
14
- className?: string; style?: React.CSSProperties; 'aria-label'?: string;
15
- };
16
-
17
- export type MetaProps = {
18
- title?: string;
19
- description?: string;
20
- lang?: string;
21
- openGraph?: { title?: string; description?: string; image?: string; url?: string; type?: string };
22
- twitter?: { card?: string; title?: string; description?: string; image?: string };
23
- icons?: { icon?: string; apple?: string };
24
- };
25
-
26
- export type FontSpec =
27
- | { kind: 'next'; fonts: Array<{ variable: string; loader: () => any }> } // Next native loaders
28
- | { kind: 'css'; hrefs: string[]; className?: string };
@@ -1,15 +0,0 @@
1
- import type { Preview } from "@storybook/react";
2
- import "../src/app/globals.css";
3
-
4
- const preview: Preview = {
5
- parameters: {
6
- controls: {
7
- matchers: {
8
- color: /(background|color)$/i,
9
- date: /Date$/i,
10
- },
11
- },
12
- },
13
- };
14
-
15
- export default preview;