Writing stories
Where stories live
Stories are colocated with the components they document, anywhere below src/<platform>/:
src/shop/client/components/atoms/facet/
├── Facet.tsx
└── Facet.stories.tsx
There is no stories glob to maintain in main.ts — discovery is automatic (see below). Two file patterns are picked up:
*.stories.js|jsx|mjs|ts|tsx— component stories*.mdx— documentation pages
How stories are discovered
Instead of a static glob, defineConfig() registers a loader that resolves stories the same way the shadowing build resolves source files, for the tenant and platform selected when you started archibald storybook:
- The loader recursively scans
src/<selected platform>/and — when the selected platform is not the default one — alsosrc/<default platform>/(the fallback tree). - Every file matching the story patterns above is collected, including files inside
tenant/<tenant>/override folders. - For matches from the default-platform fallback tree, the shadowing resolver is consulted: if the story is shadowed by a more specific file for the selected platform/tenant chain, the base story is dropped and only the override (found in step 1) is served.
The result: running Storybook for platform portal shows portal's own stories plus the default platform's stories that portal does not override — exactly the set of components that platform actually ships.
Inside stories, always import components through the base path; the babel shadowing loader rewrites the import to the most specific override at compile time:
import * as Facet from 'shop/client/components/atoms/facet/Facet';
To override a story for a specific platform, place a story file at the matching path in that platform's tree (e.g. src/portal/client/components/atoms/facet/Facet.stories.tsx) — it replaces the base story when that platform is selected.
A real example
Trimmed from the shop template's Facet.stories.tsx:
import { type Facet as FacetType } from '@archibald/search';
import type { StoryObj } from '@storybook/react';
import { fn } from 'storybook/test';
import * as Facet from 'shop/client/components/atoms/facet/Facet';
const sampleFacet: FacetType = {
name: 'Color',
visible: true,
type: 'multiSelect',
id: 'f-color',
key: 'color',
priority: 1,
values: [
{ name: 'Red', count: 12, selected: false },
{ name: 'Blue', count: 8, selected: true }
]
};
export default {
title: 'Atoms/Facet',
component: Facet.Root,
parameters: { layout: 'padded' }
};
export const Default: StoryObj = {
args: { numberOfShownFacetValues: 4, facet: sampleFacet, onApply: fn() },
render: ({ facet, numberOfShownFacetValues, onApply }) => (
<Facet.Root facet={facet} numberOfShownFacetValues={numberOfShownFacetValues} onApply={onApply} autoApply>
<Facet.List>
<Facet.MultiSelect />
</Facet.List>
<Facet.ShowMore />
</Facet.Root>
)
};
This is standard Storybook CSF — args, argTypes, render, and autodocs all work as documented upstream. The Archibald-specific part is only the base-path import.
App context with StorybookProvider
Components that use framework hooks (translations, data client, app client, router) need the same contexts they get in the application. StorybookProvider from @archibald/storybook provides them — it wraps its children in the TestingProviders used by unit tests plus an in-memory router, so Storybook and Jest share one context mechanism (see StorybookProvider in the testing docs).
The template applies it globally as a decorator in .storybook/preview.tsx, seeded with translation data:
import { DataClient, AppClient, MESSAGES_KEY } from '@archibald/core';
import { en } from '../i18n/en';
export function createI18nContext() {
const dataClient = new DataClient();
dataClient.set([MESSAGES_KEY, 'en'], {
hasBeenLoaded: true,
status: 'done',
response: en
});
const appClient = new AppClient();
appClient.language = 'en';
return { dataClient, appClient };
}
The context prop accepts the same shape as createContext — dataClient, appClient, history, translate, and friends — so a single story can also opt into its own context by wrapping its render output in another StorybookProvider with pre-seeded data (for example, CMS or API responses placed in a DataClient).
Styling
Stories run through the application's style pipeline, not a Storybook-specific one:
- The global stylesheet is imported once in
preview.tsx(import 'shop/resources/scss/base/global.scss'), giving every story the base theme. The provider also adds thethemeclass todocument.bodywhile a story is mounted. - SCSS imports resolve through the shadowing importer, so tenant/platform style overrides apply to stories exactly as in the app.
- CSS modules behave according to your
cli/style/modulesconfiguration. - With
cli/optimization/svgComponentsenabled, SVGs import as React components via SVGR, as in the app build.