@swisper.ai/b2b
v0.1.0
Published
Swisper B2B UI components
Readme
@swisper.ai/b2b
Swisper B2B UI components. They hand the deployment's sign-in mode and branding to React through an optional B2BProvider; outside it, every hook falls back to b2c with no branding, so the same app runs in both modes. The app resolves the values from its own configuration and passes them in: the package never reads env.
Boundary
Code belongs here if only the B2B products use it: a page layout or a screen part with B2B wording and behaviour fixed, such as the sign-in page layout. A primitive that any product could use belongs in @swisper.ai/ui, a card in @swisper.ai/cards-ui, and the chat surface in @swisper.ai/web-chat-ui.
Install
npm install @swisper.ai/b2b @swisper.ai/tailwind-config @swisper.ai/design-system
npm install -D tailwindcss @tailwindcss/vite| Peer | Range | Why |
| ----------- | ---------- | -------------------- |
| react | >=18.0.0 | Components and hooks |
| react-dom | >=18.0.0 | Rendering |
It depends on @swisper.ai/ui, whose peers (@mui/material, @mui/system, @mui/icons-material, @emotion/react, @emotion/styled) apply too. Add tailwindcss() from @tailwindcss/vite to the plugins of your Vite config.
Setup
Set up the styles. Add the Echo DS theme, the B2B token layer and the package sources to the top of your global stylesheet (for example
main.css):@import '@swisper.ai/tailwind-config/tailwind.css'; @import '@swisper.ai/design-system/dist/b2b.css'; /* Required for Tailwind to find class references in @swisper.ai components. */ @source '../node_modules/@swisper.ai/b2b/src'; @source '../node_modules/@swisper.ai/ui/src'; /* The rest of your application CSS */tailwind.cssbrings in Tailwind and the base theme, andb2b.cssoverrides its component tokens with the B2B values, so it comes second. The@sourcepaths are relative to this CSS file: point them at wherevernode_modulessits.Import your stylesheet before rendering any components:
// Must be imported first, so the theme and Tailwind layers are defined before any component styles import './main.css'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './App'; createRoot(document.getElementById('root')!).render( <StrictMode> <App /> </StrictMode>, );Configure the provider (optional).
B2BProvidergives every component below it the deployment'smode,loginImageUrl,logoUrlandproviderName(the customer's name, the alt text of both images). Resolve and validate them from your app's configuration; the provider renders its children immediately. Without a provider, the hooks return'b2c'and no branding.// Must be imported first, so the theme and Tailwind layers are defined before any component styles import './main.css'; import { B2BProvider } from '@swisper.ai/b2b'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './App'; createRoot(document.getElementById('root')!).render( <StrictMode> <B2BProvider mode="b2b" loginImageUrl="/branding/login.jpg" logoUrl="/branding/logo.svg" providerName="Acme" > <App /> </B2BProvider> </StrictMode>, );Read the mode and branding anywhere below it:
import { useB2BBranding, useIsB2BMode } from '@swisper.ai/b2b';
export function LogInHeader() {
const isB2B = useIsB2BMode();
const { loginImageUrl, logoUrl, providerName } = useB2BBranding();
if (!isB2B) {
return <h1>Welcome back</h1>;
}
return (
<header>
{logoUrl ? <img src={logoUrl} alt={providerName ?? ''} className="h-32" /> : null}
{loginImageUrl ? <img src={loginImageUrl} alt="" /> : null}
</header>
);
}Sign-in page
B2BLoginPageLayout is the page shell of an enterprise sign-in: a header band, the customer's logo over your card, and a footer band, over the login image. Combine the parts you need; the login image and the logo come from the nearest B2BProvider, never from props. Below sm, or without an image, the page is plain bg-muted. The card and its h1 are yours, and so is the favicon: set it in your HTML or head manager.
import {
B2BLoginPageLayout,
B2BLoginPageLayoutBrand,
B2BLoginPageLayoutContent,
B2BLoginPageLayoutFooter,
B2BLoginPageLayoutHeader,
B2BLoginPageLayoutHeaderDescription,
B2BLoginPageLayoutHeaderTitle,
B2BLoginPageLayoutPoweredBy,
} from '@swisper.ai/b2b';
import { Link } from '@swisper.ai/ui';
export function SignInPage() {
return (
<B2BLoginPageLayout>
<B2BLoginPageLayoutHeader>
<B2BLoginPageLayoutHeaderTitle>Signing in to Swisper</B2BLoginPageLayoutHeaderTitle>
<B2BLoginPageLayoutHeaderDescription>
Use your company account to access Swisper.
</B2BLoginPageLayoutHeaderDescription>
</B2BLoginPageLayoutHeader>
<B2BLoginPageLayoutContent>
{/* The provider's logo; set href to make it a link. */}
<B2BLoginPageLayoutBrand />
<SignInCard />
</B2BLoginPageLayoutContent>
<B2BLoginPageLayoutFooter>
<Link href="/privacy">Privacy</Link>
<Link href="/terms">Terms</Link>
<B2BLoginPageLayoutPoweredBy href="https://swisper.ai" />
</B2BLoginPageLayoutFooter>
</B2BLoginPageLayout>
);
}Styles
The setup above builds the package's classes into your app's own Tailwind CSS. @swisper.ai/tailwind-config/tailwind.css already scans the other @swisper.ai component packages when they are installed next to it; the @source lines cover layouts, such as pnpm's, where they are not.
Without a Tailwind build
@swisper.ai/b2b/index.css is a prebuilt stylesheet: Tailwind's preflight, the base design-system theme and every utility the @swisper.ai packages use. It does not include the B2B token layer, so load both once, at the app's entry, in this order:
import '@swisper.ai/b2b/index.css';
import '@swisper.ai/design-system/dist/b2b.css';Use one setup, not both. If your app also loads the prebuilt index.css of another @swisper.ai package, or builds its own Tailwind CSS next to it, the same rules ship twice and whichever file loads last wins where they conflict.
API
Props and live examples are in the Echo DS Storybook, under B2B.
| Export | Why it exists |
| ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| B2BProvider, B2BProviderProps | Hands the deployment's sign-in mode, login image, logo and provider name to React once, so components read them instead of each app threading them through |
| useB2BContext | The whole value from the nearest B2BProvider: b2c with no branding when there is none |
| useAuthMode, useIsB2BMode | The sign-in mode, and true only inside a provider whose mode is 'b2b', so b2b-only UI has one check |
| useB2BBranding | The loginImageUrl, logoUrl and providerName from the nearest provider, all null when there is none |
| AuthMode, B2BContextValue | The 'b2c' \| 'b2b' mode and the provider's value |
| B2BLoginPageLayout, B2BLoginPageLayoutContent | The sign-in page over the provider's login image, and its main column (up to max-w-sm) for the brand and your card |
| B2BLoginPageLayoutHeader, B2BLoginPageLayoutHeaderTitle, B2BLoginPageLayoutHeaderLogo, B2BLoginPageLayoutHeaderDescription | The band that says what the user signs in to, with the product's logo inline |
| B2BLoginPageLayoutBrand | The customer's logo above the card: its children, else the provider's logo; a link while href is set |
| B2BLoginPageLayoutFooter | The band for brief links, such as privacy and terms |
| B2BLoginPageLayoutPoweredBy | The "Powered by Swisper" pill, last in the footer: right of the links from sm up; a link while href is set |
| B2BLoginPageLayoutProps, B2BLoginPageLayoutHeaderProps, … | The props of each part |
Troubleshooting
| Symptom | Cause | Fix |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| "Invalid hook call", or a context is missing | The app resolves two copies of React | Add resolve: { dedupe: ['react', 'react-dom'] } to the Vite config |
| useIsB2BMode() is always false, or the logo never shows | No B2BProvider sits above the component, or its mode is not 'b2b' | Wrap the app in B2BProvider with mode="b2b" (Setup, step 3) |
| Components render without layout or colors | Tailwind never scans the package, because it skips node_modules by default | Import @swisper.ai/tailwind-config/tailwind.css and add the @source lines from Setup, step 1, or load the prebuilt index.css instead |
| Buttons and banners have the B2C colours and radii | The B2B token layer isn't loaded | Import @swisper.ai/design-system/dist/b2b.css after the theme or the package's CSS |
| Can't resolve '@swisper.ai/design-system/b2b.css' | The layer is exported under dist/ | Import @swisper.ai/design-system/dist/b2b.css |
| Your own base styles (fonts, headings, resets) get overridden | The prebuilt index.css is loaded next to your own Tailwind build, and its preflight wins | Use one of the two: drop index.css and keep the Tailwind setup |
| Spacing looks wrong (huge gaps, tiny icons) | A number in a spacing class is pixels: gap-8 is 8px, not Tailwind's default 32px | Write the pixel value you want |
| Styles are missing or doubled, or the MUI theme doesn't apply | The app resolves two copies of @emotion/react or @mui/material | Add them to resolve.dedupe in the Vite config, next to react |
| ERR_UNSUPPORTED_DIR_IMPORT when a test imports the package | dist/ uses extensionless imports that only a bundler resolves, and Vitest externalizes it | Set server.deps.inline: [/@swisper\.ai\//] in the Vitest config |
