Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 42 additions & 6 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const __dirname = dirname(__filename);

Comment thread
jaieds marked this conversation as resolved.
/** @type { import('@storybook/react-webpack5').StorybookConfig } */
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)', '../src/**/*.mdx'],
Comment thread
jaieds marked this conversation as resolved.
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-links',
Expand All @@ -33,16 +33,52 @@ const config: StorybookConfig = {
// Merge custom configuration into the default config
const { mergeConfig } = await import('vite');

// Remove the dts plugin from the default config.
// Remove library-build-only plugins that must not run in the Storybook
// preview build: vite:dts (type emit) and preserve-directives (which
Comment thread
jaieds marked this conversation as resolved.
// interferes with MDX's mdx-react-shim import resolution).
// Remove library-build-only plugins that must not run in the Storybook
// preview build: vite:dts (type emit) and preserve-directives (which
// interferes with MDX's mdx-react-shim import resolution).
const libOnlyPlugins = [ 'vite:dts', 'preserve-directives' ];
config.plugins = [
...(config.plugins ?? []).filter((plugin) => {
return (
(plugin as typeof plugin & Record<string, unknown>).name !==
'vite:dts'
);
const name = (plugin as typeof plugin & Record<string, unknown>)
.name as string | undefined;
return ! name || ! libOnlyPlugins.includes(name);
}),
];

// The library build config (lib mode + externalized devDependencies +
// preserveModules output) must not leak into the Storybook preview
// build. In particular, externalizing @storybook/addon-docs breaks the
Comment thread
jaieds marked this conversation as resolved.
// MDX `mdx-react-shim` import. Storybook bundles its own deps, so reset
// these to its defaults.
if (config.build) {
delete config.build.lib;
config.build.rollupOptions = {};
}

// Workaround: @storybook/addon-docs emits the MDX runtime shim import as
// a malformed `file://./node_modules/...mdx-react-shim.js` specifier,
// which Rollup cannot resolve during `build-storybook`. Normalize any
// file:// id back to an absolute filesystem path so it resolves.
config.plugins = [
{
name: 'force-ui-normalize-file-url-imports',
enforce: 'pre' as const,
resolveId(source: string) {
if (source.startsWith('file://')) {
return path.resolve(
process.cwd(),
source.slice('file://'.length)
);
}
return null;
},
},
...(config.plugins ?? []),
];

return mergeConfig(config, {
optimizeDeps: {
...config?.optimizeDeps,
Expand Down
3 changes: 3 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
Version 1.8.0 - 30th June, 2026
- New: Added Next.js React Server Components support. Every component now ships the `"use client"` directive in its build output, and compound components additionally expose named subpart exports (e.g. `SelectButton`, `DialogPanel`) so they can be rendered directly inside Server Components.

Version 1.7.13 - 11th June, 2026
- Fix: Resolved `patch-package: command not found` install failure when the library is consumed as a git dependency. `patch-package` is now a runtime dependency so the bundled Lexical Shadow DOM patches are applied in consumer projects, which is required for the Editor Input mention menu Shadow DOM fix to work.

Expand Down
67 changes: 67 additions & 0 deletions src/components/accordion/accordion.nextjs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
import * as Stories from './accordion.stories';
import * as NextjsStories from './accordion.nextjs.stories';

<Meta of={Stories} name="Next.js Usage" />

# Accordion — Next.js (App Router)

> **TL;DR** — `Accordion` works in the Next.js App Router out of the box: import it and render it inside a **Server Component**. Use its named subpart exports (e.g. `AccordionItem`) — not dot notation (`Accordion.X`). Put anything interactive (event handlers, state) in a file that starts with `'use client'`.

## Usage in a Server Component

Use the **named subpart exports** — not dot access (`Accordion.X`). In a Server Component, React cannot read sub-components attached to a client reference, so dot access resolves to `undefined`. The named exports are individual client references:

```tsx
// app/page.tsx — Server Component
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@bsf/force-ui';

export default function Page() {
return (
<Accordion>
<AccordionItem value="a">
<AccordionTrigger>What is force-ui?</AccordionTrigger>
<AccordionContent>A React + Tailwind component library.</AccordionContent>
</AccordionItem>
</Accordion>
);
}
```

## Dot notation (client components only)

Inside your own client components the familiar compound dot syntax still works — it is only Server Components that require the named exports above:

```tsx
'use client';
import { Accordion } from '@bsf/force-ui';

<Accordion>
<Accordion.Item value="a">
<Accordion.Trigger>Title</Accordion.Trigger>
<Accordion.Content>Body</Accordion.Content>
</Accordion.Item>
</Accordion>
```

## Event handlers & state

Server Components can't hold state or receive event handlers (`onClick`, `onChange`, …). Move the interactive part into its own client component:

```tsx
'use client';
import { Accordion } from '@bsf/force-ui';

export default function Interactive() {
// useState, onClick, onChange … live in this client file
return <Accordion />;
}
```

Then import that into your Server Component — the page itself stays a Server Component.

## Live preview

_Rendered with the named (App-Router) pattern._

<Canvas of={NextjsStories.ServerComponentPattern} />
30 changes: 30 additions & 0 deletions src/components/accordion/accordion.nextjs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
Comment thread
jaieds marked this conversation as resolved.
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components';

// Powers the "Next.js Usage" docs <Canvas>. Hidden from the sidebar
// (`!dev`) and excluded from autodocs — it only demonstrates the named
// (App-Router) import pattern in an isolated preview with copyable code.
const meta: Meta = {
title: 'Molecules/Accordion/Next.js Example',
component: Accordion,
tags: [ '!dev', '!autodocs' ],
parameters: { layout: 'centered' },
};

export default meta;

export const ServerComponentPattern: StoryObj = {
Comment thread
jaieds marked this conversation as resolved.
name: 'Named exports (App Router)',
render: () => {
return (
<div className="w-full max-w-lg">
<Accordion>
Comment thread
jaieds marked this conversation as resolved.
<AccordionItem value="a">
<AccordionTrigger>What is force-ui?</AccordionTrigger>
<AccordionContent>A React + Tailwind component library.</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
},
};
Comment thread
jaieds marked this conversation as resolved.
1 change: 1 addition & 0 deletions src/components/accordion/accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What: The addition of the 'use client' directive is correct, but ensure consistent application across all components that directly interact with client-side functionalities.

Why: The 'use client' directive is necessary for Next.js Server Components to correctly interpret component behavior. This helps to prevent potential rendering issues when components need to access client-specific behavior, such as DOM manipulation or state management.

How: Double-check that this directive is present for all components intended to be rendered as client components. Review all component files to ensure consistency and functionality.

import React, {
type ReactNode,
type ElementType,
Expand Down
5 changes: 5 additions & 0 deletions src/components/accordion/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export { default } from './accordion';
export {
Comment thread
jaieds marked this conversation as resolved.
AccordionItem,
AccordionTrigger,
AccordionContent,
} from './accordion';
39 changes: 39 additions & 0 deletions src/components/alert/alert.nextjs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
import * as Stories from './alert.stories';

<Meta of={Stories} name="Next.js Usage" />

# Alert — Next.js (App Router)

> **TL;DR** — `Alert` works in the Next.js App Router out of the box: import it and render it inside a **Server Component**. Put anything interactive (event handlers, state) in a file that starts with `'use client'`.

## Usage in a Server Component

```tsx
// app/page.tsx — Server Component
import { Alert } from '@bsf/force-ui';

export default function Page() {
return <Alert />;
}
```

## Event handlers & state

Server Components can't hold state or receive event handlers (`onClick`, `onChange`, …). Move the interactive part into its own client component:

```tsx
'use client';
import { Alert } from '@bsf/force-ui';

export default function Interactive() {
// useState, onClick, onChange … live in this client file
return <Alert />;
}
```

Then import that into your Server Component — the page itself stays a Server Component.

## Live preview

<Canvas of={Stories.Neutral} />
1 change: 1 addition & 0 deletions src/components/alert/alert.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client';
import { cn } from '@/utilities/functions';
import { getIcon, getAction, getContent, getTitle } from '../toaster/utils';
import { X } from 'lucide-react';
Expand Down
43 changes: 43 additions & 0 deletions src/components/area-chart/area-chart.nextjs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
import * as Stories from './area-chart.stories';

<Meta of={Stories} name="Next.js Usage" />

# AreaChart — Next.js (App Router)

> **TL;DR** — `AreaChart` works in the Next.js App Router out of the box: import it and render it inside a **Server Component**. Put anything interactive (event handlers, state) in a file that starts with `'use client'`.

## Usage in a Server Component

```tsx
// app/page.tsx — Server Component
import { AreaChart } from '@bsf/force-ui';

export default function Page() {
return <AreaChart />;
}
```

## Notes

- Chart data is passed as props; renders client-side after hydration.

## Event handlers & state

Server Components can't hold state or receive event handlers (`onClick`, `onChange`, …). Move the interactive part into its own client component:

```tsx
'use client';
import { AreaChart } from '@bsf/force-ui';

export default function Interactive() {
// useState, onClick, onChange … live in this client file
return <AreaChart />;
}
```

Then import that into your Server Component — the page itself stays a Server Component.

## Live preview

<Canvas of={Stories.AreaChartSimple} />
1 change: 1 addition & 0 deletions src/components/area-chart/area-chart.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client';
Comment thread
jaieds marked this conversation as resolved.
import { useEffect, useState, type ReactNode } from 'react';
import {
AreaChart as AreaChartWrapper,
Expand Down
39 changes: 39 additions & 0 deletions src/components/avatar/avatar.nextjs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
import * as Stories from './avatar.stories';

<Meta of={Stories} name="Next.js Usage" />

# Avatar — Next.js (App Router)

> **TL;DR** — `Avatar` works in the Next.js App Router out of the box: import it and render it inside a **Server Component**. Put anything interactive (event handlers, state) in a file that starts with `'use client'`.

## Usage in a Server Component

```tsx
// app/page.tsx — Server Component
import { Avatar } from '@bsf/force-ui';

export default function Page() {
return <Avatar />;
}
```

## Event handlers & state

Server Components can't hold state or receive event handlers (`onClick`, `onChange`, …). Move the interactive part into its own client component:

```tsx
'use client';
import { Avatar } from '@bsf/force-ui';

export default function Interactive() {
// useState, onClick, onChange … live in this client file
return <Avatar />;
}
```

Then import that into your Server Component — the page itself stays a Server Component.

## Live preview

<Canvas of={Stories.Default} />
1 change: 1 addition & 0 deletions src/components/avatar/avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client';
Comment thread
jaieds marked this conversation as resolved.
import React, { forwardRef, useEffect, useState, type ReactNode } from 'react';
import { cn } from '@/utilities/functions';
import { User } from 'lucide-react';
Expand Down
39 changes: 39 additions & 0 deletions src/components/badge/badge.nextjs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
import * as Stories from './badge.stories';

<Meta of={Stories} name="Next.js Usage" />

# Badge — Next.js (App Router)

> **TL;DR** — `Badge` works in the Next.js App Router out of the box: import it and render it inside a **Server Component**. Put anything interactive (event handlers, state) in a file that starts with `'use client'`.

## Usage in a Server Component

```tsx
// app/page.tsx — Server Component
import { Badge } from '@bsf/force-ui';

export default function Page() {
return <Badge />;
}
```

## Event handlers & state

Server Components can't hold state or receive event handlers (`onClick`, `onChange`, …). Move the interactive part into its own client component:

```tsx
'use client';
import { Badge } from '@bsf/force-ui';

export default function Interactive() {
// useState, onClick, onChange … live in this client file
return <Badge />;
}
```

Then import that into your Server Component — the page itself stays a Server Component.

## Live preview

<Canvas of={Stories.Neutral} />
1 change: 1 addition & 0 deletions src/components/badge/badge.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client';
Comment thread
jaieds marked this conversation as resolved.
import { forwardRef, type ReactNode } from 'react';
import { cn } from '@/utilities/functions';
import { X } from 'lucide-react';
Expand Down
Loading
Loading