Skip to content
Merged
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
16 changes: 15 additions & 1 deletion packages/document/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,22 @@ import remarkGfm from "remark-gfm"
import { type UserConfig, mergeConfig } from "vite"
import tsconfigPaths from "vite-tsconfig-paths"

const rcSrc = resolve(import.meta.dirname, "../../react-components/src")

const viteOverrides: UserConfig = {
base: process.env.VITE_BASE_URL,
resolve: {
alias: {
"#components": `${rcSrc}/components`,
"#components-utils": `${rcSrc}/components/utils`,
"#context": `${rcSrc}/context`,
"#hooks": `${rcSrc}/hooks`,
"#typings": `${rcSrc}/typings`,
"#utils": `${rcSrc}/utils`,
"#config": `${rcSrc}/config`,
"#reducers": `${rcSrc}/reducers`,
},
},
plugins: [
tsconfigPaths({
projects: [
Expand All @@ -20,7 +34,7 @@ const storybookConfig: StorybookConfig = {
async viteFinal(config) {
return mergeConfig(config, viteOverrides)
},
stories: ["../stories/**/*.mdx", "../stories/**/*.stories.@(js|jsx|ts|tsx)"],
stories: ["../src/stories/**/*.mdx", "../src/stories/**/*.stories.@(js|jsx|ts|tsx)"],
addons: ["@storybook/addon-links", {
name: "@storybook/addon-docs",
options: {
Expand Down
20 changes: 12 additions & 8 deletions packages/document/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,34 @@
"mcp": "storybook mcp"
},
"dependencies": {
"@commercelayer/js-auth": "^7.1.2",
"@commercelayer/react-components": "workspace:*",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"react": "^19.2.3",
"react-dom": "^19.2.3"
},
"devDependencies": {
"@types/js-cookie": "^3.0.6",
"@chromatic-com/storybook": "^5.1.1",
"@eslint/js": "^9.39.2",
"@storybook/addon-docs": "^10.3.3",
"@storybook/addon-links": "^10.3.3",
"@storybook/addon-docs": "^10.3.5",
"@storybook/addon-links": "^10.3.5",
"@storybook/addon-mcp": "^0.4.2",
"@storybook/addon-onboarding": "^10.3.3",
"@storybook/addon-onboarding": "^10.3.5",
"@storybook/icons": "^2.0.1",
"@storybook/react": "^10.3.3",
"@storybook/react-vite": "^10.3.3",
"@storybook/react-vite": "^10.3.5",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"eslint": "^9.39.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-storybook": "^10.3.3",
"eslint-plugin-storybook": "^10.3.5",
"globals": "^17.0.0",
"msw": "^2.12.7",
"remark-gfm": "^4.0.1",
"storybook": "^10.3.3",
"storybook": "^10.3.5",
"typescript": "~5.9.3",
"typescript-eslint": "^8.53.0",
"vite": "^7.3.1",
Expand All @@ -46,4 +50,4 @@
"plugin:storybook/recommended"
]
}
}
}
155 changes: 155 additions & 0 deletions packages/document/src/stories/availability/001.availability.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { Meta, Source } from '@storybook/addon-docs';

<Meta title="Availability/Overview" />

# Availability

The Availability components let you display real-time stock quantity and delivery lead times
for any SKU. They are powered by the Commerce Layer inventory model and work by fetching
availability data through the `useAvailability` hook from `@commercelayer/hooks`.

All Availability components must be nested inside the `<CommerceLayer>` context.

---

## AvailabilityContainer

`AvailabilityContainer` is the root component of the Availability tree.
It fetches inventory data for a given SKU (by code or ID) and exposes the result
to its children through the Availability context.

<span title='Requirements' type='warning'>
Must be a child of the `<CommerceLayer>` component.
Can also be a child of `<Skus>` inside a `<SkusContainer>`, in which case `skuCode` is inherited automatically.
</span>

<span title='Children' type='info'>
`<AvailabilityTemplate>`
</span>

**Props**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `skuCode` | `string` | β€” | The SKU code to fetch availability for |
| `skuId` | `string` | β€” | The SKU ID (takes precedence over `skuCode`; improves performance) |
| `getQuantity` | `(quantity: number) => void` | β€” | Callback fired whenever the available quantity changes |

<Source
language="jsx"
dark
code={`
import {
CommerceLayer,
AvailabilityContainer,
AvailabilityTemplate,
} from '@commercelayer/react-components'

<CommerceLayer accessToken="..." endpoint="https://yourdomain.commercelayer.io">
<AvailabilityContainer skuCode="TSHIRTMM000000FFFFFFXLXX">
<AvailabilityTemplate />
</AvailabilityContainer>
</CommerceLayer>
`}
/>

---

## AvailabilityTemplate

`AvailabilityTemplate` reads from the parent `AvailabilityContainer` context and renders
a `<span>` with availability text. You can customise the label shown for each state
(`available`, `outOfStock`, `negativeStock`) and optionally include delivery lead time
and shipping method details.

<span title='Requirements' type='warning'>
Must be a descendant of the `<AvailabilityContainer>` component.
</span>

**Props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `labels.available` | `string` | `"Available"` | Text shown when quantity > 0 |
| `labels.outOfStock` | `string` | `"Out of stock"` | Text shown when quantity is 0 |
| `labels.negativeStock` | `string` | `"Not available"` | Text shown when quantity is negative |
| `timeFormat` | `"days" \| "hours"` | β€” | When set, delivery lead time is appended to the label |
| `showShippingMethodName` | `boolean` | `false` | Requires `timeFormat`. Appends the shipping method name |
| `showShippingMethodPrice` | `boolean` | `false` | Requires `timeFormat`. Appends the formatted shipping price |

<Source
language="jsx"
dark
code={`
<AvailabilityContainer skuCode="TSHIRTMM000000FFFFFFXLXX">
<AvailabilityTemplate
labels={{
available: 'In stock',
outOfStock: 'Sold out',
}}
timeFormat="days"
showShippingMethodName
showShippingMethodPrice
/>
</AvailabilityContainer>
`}
/>

### Custom render via children

You can fully control the rendered output by passing a function as `children`.
The function receives the full availability context including `quantity`, `text`,
`min`, `max`, and `shipping_method`.

<Source
language="jsx"
dark
code={`
<AvailabilityContainer skuCode="TSHIRTMM000000FFFFFFXLXX">
<AvailabilityTemplate>
{({ quantity, text, min, max }) => (
<div>
<strong>{text}</strong>
{quantity > 0 && min != null && (
<p>Ships in {min.days}–{max?.days ?? min.days} days</p>
)}
</div>
)}
</AvailabilityTemplate>
</AvailabilityContainer>
`}
/>

---

## Usage inside SkusContainer

When used inside a `<SkusContainer>` β†’ `<Skus>` tree, `AvailabilityContainer`
automatically inherits the `skuCode` from the current SKU context β€”
no need to pass `skuCode` explicitly.

<Source
language="jsx"
dark
code={`
import {
CommerceLayer,
SkusContainer,
Skus,
SkuField,
AvailabilityContainer,
AvailabilityTemplate,
} from '@commercelayer/react-components'

<CommerceLayer accessToken="..." endpoint="https://yourdomain.commercelayer.io">
<SkusContainer skus={['TSHIRTMM000000FFFFFFXLXX', 'PANTSMM000000FFFFFFXXXX']}>
<Skus>
<SkuField attribute="name" tagElement="h3" />
<AvailabilityContainer>
<AvailabilityTemplate />
</AvailabilityContainer>
</Skus>
</SkusContainer>
</CommerceLayer>
`}
/>
160 changes: 160 additions & 0 deletions packages/document/src/stories/availability/availability.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import CommerceLayer from '../_internals/CommerceLayer'
import {
AvailabilityContainer,
AvailabilityTemplate,
SkusContainer,
Skus,
SkuField,
} from '@commercelayer/react-components'

const meta = {
title: 'Availability/Stories',
parameters: {
layout: 'centered',
},
} satisfies Meta

export default meta
type Story = StoryObj<typeof meta>

export const BasicAvailability: Story = {
name: 'AvailabilityContainer β€” basic',
render: () => (
<CommerceLayer accessToken="my-access-token">
<AvailabilityContainer skuCode="POLOMXXX000000FFFFFFLXXX">
<AvailabilityTemplate />
</AvailabilityContainer>
</CommerceLayer>
),
}

export const CustomLabels: Story = {
name: 'AvailabilityTemplate β€” custom labels',
render: () => (
<CommerceLayer accessToken="my-access-token">
<AvailabilityContainer skuCode="POLOMXXX000000FFFFFFLXXX">
<AvailabilityTemplate
labels={{
available: 'βœ… In stock',
outOfStock: '❌ Sold out',
negativeStock: '⚠️ Not available',
}}
/>
</AvailabilityContainer>
</CommerceLayer>
),
}

export const WithDeliveryLeadTimeDays: Story = {
name: 'AvailabilityTemplate β€” lead time in days',
render: () => (
<CommerceLayer accessToken="my-access-token">
<AvailabilityContainer skuCode="POLOMXXX000000FFFFFFLXXX">
<AvailabilityTemplate
labels={{ available: 'Available' }}
timeFormat="days"
/>
</AvailabilityContainer>
</CommerceLayer>
),
}

export const WithDeliveryLeadTimeHours: Story = {
name: 'AvailabilityTemplate β€” lead time in hours',
render: () => (
<CommerceLayer accessToken="my-access-token">
<AvailabilityContainer skuCode="POLOMXXX000000FFFFFFLXXX">
<AvailabilityTemplate
labels={{ available: 'Available' }}
timeFormat="hours"
/>
</AvailabilityContainer>
</CommerceLayer>
),
}

export const WithShippingMethodName: Story = {
name: 'AvailabilityTemplate β€” with shipping method name',
render: () => (
<CommerceLayer accessToken="my-access-token">
<AvailabilityContainer skuCode="POLOMXXX000000FFFFFFLXXX">
<AvailabilityTemplate
timeFormat="days"
showShippingMethodName
/>
</AvailabilityContainer>
</CommerceLayer>
),
}

export const WithShippingMethodPrice: Story = {
name: 'AvailabilityTemplate β€” with shipping method price',
render: () => (
<CommerceLayer accessToken="my-access-token">
<AvailabilityContainer skuCode="POLOMXXX000000FFFFFFLXXX">
<AvailabilityTemplate
timeFormat="days"
showShippingMethodName
showShippingMethodPrice
/>
</AvailabilityContainer>
</CommerceLayer>
),
}

export const WithGetQuantityCallback: Story = {
name: 'AvailabilityContainer β€” getQuantity callback',
render: () => (
<CommerceLayer accessToken="my-access-token">
<AvailabilityContainer
skuCode="POLOMXXX000000FFFFFFLXXX"
getQuantity={(quantity) => {
console.log('quantity updated:', quantity)
}}
>
<AvailabilityTemplate />
</AvailabilityContainer>
</CommerceLayer>
),
}

export const WithChildrenRenderProp: Story = {
name: 'AvailabilityTemplate β€” children render prop',
render: () => (
<CommerceLayer accessToken="my-access-token">
<AvailabilityContainer skuCode="POLOMXXX000000FFFFFFLXXX">
<AvailabilityTemplate>
{({ quantity, text, min, max }) => (
<div style={{ fontFamily: 'monospace', fontSize: 14 }}>
<strong>{text}</strong>
{quantity > 0 && min != null && (
<p style={{ marginTop: 4, color: '#666' }}>
Ships in {min.days}–{max?.days ?? min.days} day(s)
</p>
)}
</div>
)}
</AvailabilityTemplate>
</AvailabilityContainer>
</CommerceLayer>
),
}

export const InsideSkusContainer: Story = {
name: 'AvailabilityContainer β€” inside SkusContainer',
render: () => (
<CommerceLayer accessToken="my-access-token">
<SkusContainer skus={['POLOMXXX000000FFFFFFLXXX', 'TSHIRTMM000000FFFFFFXLXX']}>
<Skus>
<div style={{ marginBottom: 16 }}>
<SkuField attribute="name" tagElement="h3" style={{ marginBottom: 4 }} />
<AvailabilityContainer>
<AvailabilityTemplate />
</AvailabilityContainer>
</div>
</Skus>
</SkusContainer>
</CommerceLayer>
),
}
Loading
Loading