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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,28 @@ yarn-error.log*
.idea
.vscode
.cursor
agent-os
AGENTS.md
CLAUDE.md
.claude

# Local history
.lh

# Storybook
*storybook.log
storybook-static

# Dist folders
/dist

# AI rules
.ai
.claude
.ai
AGENTS.md
CLAUDE.md
agent-os

# Generated files
O2S-API.postman_collection.json
1 change: 1 addition & 0 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx --no -- commitlint --edit $1
2 changes: 2 additions & 0 deletions .prettierrc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const config = {
...apiConfig,
...webConfig,
...uiConfig,
// Required for .tsx files - parser was misinterpreting JSX as regex
importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
};

export default config;
41 changes: 41 additions & 0 deletions .storybook/Introduction.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="Introduction" />

<a href="https://www.openselfservice.com" style={{ display: 'inline-block', marginBottom: '20px' }}>
<img
src="/o2s-gh-cover.png"
alt="Open Self Service - open-source development kit for composable Customer Portals"
/>
</a>

# Welcome to O2S UI Library

This Storybook is the living documentation for **Open Self Service (O2S)** — an open-source, composable frontend framework for building customer portals and self-service applications. Here you can browse and test UI components, blocks, and elements in isolation.

## What's in this Storybook

The sidebar is organized into three main sections:

- **Blocks** — Reusable block components from `packages/blocks`. Each block is self-contained (backend + frontend + SDK) and can be used in the customer portal. Think of them as ready‑made business features (for example ticket lists, product views, knowledge-base articles) that already handle data fetching and integration details for you.
- **Components** — Higher-level UI components from the shared `@o2s/ui` package (e.g. cards, filters, data grids). They are used to build layouts, pages, and sections – more generic than blocks, but still opinionated to match the O2S design system and behavior.
- **Elements** — Base UI elements (buttons, inputs, dialogs, etc.) from `@o2s/ui`, built on shadcn/ui and Tailwind CSS. These are the building blocks of the library – low-level controls you can compose into your own components and blocks when you need more custom behavior.

Use the sidebar to expand any section and open a component’s docs and stories.

## Public instance

A public Storybook is available and kept in sync with the latest version:

**[https://storybook-o2s.openselfservice.com/](https://storybook-o2s.openselfservice.com/)**

You can explore components there without running the repo locally.

## Testing

Stories are also used as the primary surface for automated tests via Vitest and the `@storybook/addon-vitest` integration. This allows us to run browser-based checks (including accessibility via the a11y addon) against the same stories you see here.

## Learn more

- [O2S documentation](https://openselfservice.com) — Project overview, getting started, and guides.
- [Storybook docs](https://storybook.js.org/docs) — How to write stories and configure Storybook.
30 changes: 26 additions & 4 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { StorybookConfig } from '@storybook/nextjs-vite';
import tailwindcss from '@tailwindcss/postcss';
import react from '@vitejs/plugin-react';
import * as dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { mergeConfig } from 'vite';
import svgr from 'vite-plugin-svgr';
import tsconfigPaths from 'vite-tsconfig-paths';
Expand All @@ -23,11 +23,19 @@ dotenv.config({

const config: StorybookConfig = {
stories: [
'./Introduction.mdx',
'../apps/frontend/src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
'../packages/blocks/**/src/frontend/**/*.stories.@(js|jsx|mjs|ts|tsx)',
'../packages/ui/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
addons: ['@storybook/addon-docs', '@storybook/addon-a11y', '@storybook/addon-themes', '@storybook/addon-vitest'],
staticDirs: ['./public'],
addons: [
'@storybook/addon-docs',
'@storybook/addon-a11y',
'@storybook/addon-themes',
'@storybook/addon-vitest',
'msw-storybook-addon',
],
framework: {
name: '@storybook/nextjs-vite',
options: {},
Expand Down Expand Up @@ -63,14 +71,28 @@ const config: StorybookConfig = {
},
},
optimizeDeps: {
include: ['@o2s/framework/modules', '@o2s/framework/sdk'],
include: [
'@o2s/framework/modules',
'@o2s/framework/sdk',
'storybook/test',
'react',
'react-dom',
'throttle-debounce',
'@radix-ui/react-collapsible',
'@radix-ui/react-popover',
'@radix-ui/react-progress',
'@radix-ui/react-radio-group',
],
},
resolve: {
conditions: ['import', 'module', 'browser', 'default'],
alias: {
'@o2s/configs.integrations/live-preview': path.resolve(__dirname, './mocks/live-preview.mock.ts'),
'@o2s/framework/sdk': path.resolve(__dirname, '../packages/framework/src/sdk.ts'),
'@o2s/framework/modules': path.resolve(__dirname, '../packages/framework/src/index.ts'),
'@o2s/ui': path.resolve(__dirname, '../packages/ui/src'),
'@o2s/ui/components': path.resolve(__dirname, '../packages/ui/src/components'),
'@o2s/utils.api-harmonization': path.resolve(__dirname, './mocks/utils.api-harmonization.mock.ts'),
},
},
ssr: {
Expand Down
17 changes: 17 additions & 0 deletions .storybook/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { addons } from 'storybook/manager-api';
import { create } from 'storybook/theming';

const o2sTheme = create({
base: 'light',
brandTitle: 'O2S',
brandUrl: 'https://storybook-o2s.openselfservice.com/',
brandImage: '/logo.svg',
brandTarget: '_blank',
});

addons.setConfig({
theme: o2sTheme,
sidebar: {
collapsedRoots: ['blocks', 'components', 'elements'],
},
});
219 changes: 219 additions & 0 deletions .storybook/mocks/data/cart-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/**
* Shared mock cart and checkout data for Storybook.
* Used by cart and checkout block SDK mocks.
*/

const MOCK_CART = {
id: 'storybook-cart-1',
customerId: 'cust-001',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
currency: 'EUR' as const,
regionId: 'reg-1',
items: {
data: [
{
id: 'item-1',
sku: 'SKU-001',
quantity: 2,
price: { value: 49.99, currency: 'EUR' as const },
subtotal: { value: 99.98, currency: 'EUR' as const },
discountTotal: { value: 0, currency: 'EUR' as const },
total: { value: 99.98, currency: 'EUR' as const },
unit: 'PCS' as const,
currency: 'EUR' as const,
product: {
id: 'prod-1',
sku: 'SKU-001',
name: 'Wireless Noise-Cancelling Headphones',
description: 'Premium over-ear headphones with active noise cancellation and 30h battery life',
shortDescription: 'Over-ear ANC headphones',
image: {
url: 'https://raw.githubusercontent.com/o2sdev/openselfservice/refs/heads/main/packages/integrations/mocked/public/images/prd-004_1.jpg',
width: 800,
height: 800,
alt: 'Wireless Noise-Cancelling Headphones',
},
price: { value: 49.99, currency: 'EUR' as const },
link: '/products/sample',
type: 'PHYSICAL' as const,
category: 'General',
tags: [],
},
},
{
id: 'item-2',
sku: 'SKU-002',
quantity: 1,
price: { value: 105, currency: 'EUR' as const },
subtotal: { value: 105, currency: 'EUR' as const },
discountTotal: { value: 0, currency: 'EUR' as const },
total: { value: 105, currency: 'EUR' as const },
unit: 'PCS' as const,
currency: 'EUR' as const,
product: {
id: 'prod-2',
sku: 'SKU-002',
name: 'USB-C Charging Cable (2m)',
description: 'Braided USB-C to USB-C fast charging cable, 2 meters',
shortDescription: 'USB-C charging cable',
image: {
url: 'https://raw.githubusercontent.com/o2sdev/openselfservice/refs/heads/main/packages/integrations/mocked/public/images/prd-005_1.jpg',
width: 800,
height: 800,
alt: 'USB-C Charging Cable',
},
price: { value: 105, currency: 'EUR' as const },
link: '/products/another',
type: 'PHYSICAL' as const,
category: 'General',
tags: [],
},
},
],
total: 2,
},
subtotal: { value: 204.98, currency: 'EUR' as const },
discountTotal: { value: 0, currency: 'EUR' as const },
taxTotal: { value: 47.14, currency: 'EUR' as const },
shippingTotal: { value: 0, currency: 'EUR' as const },
total: { value: 252.12, currency: 'EUR' as const },
billingAddress: {
firstName: 'John',
lastName: 'Doe',
companyName: 'ACME Inc.',
taxId: '1234567890',
country: 'PL',
streetName: 'Main Street',
streetNumber: '123',
city: 'Warsaw',
postalCode: '00-001',
email: 'john@example.com',
phone: '+48 123 456 789',
},
shippingAddress: {
firstName: 'John',
lastName: 'Doe',
country: 'PL',
streetName: 'Main Street',
streetNumber: '123',
city: 'Warsaw',
postalCode: '00-001',
phone: '+48 123 456 789',
},
shippingMethod: {
id: 'standard',
name: 'Standard Shipping',
description: '3-5 business days',
total: { value: 0, currency: 'EUR' as const },
subtotal: { value: 0, currency: 'EUR' as const },
},
paymentMethod: {
id: 'card',
name: 'Credit Card',
description: 'Pay with Visa, Mastercard',
},
promotions: [
{
id: 'promo-1',
code: 'SAVE10',
name: '10% Off',
type: 'PERCENTAGE' as const,
value: '10',
},
],
notes: '',
email: 'john@example.com',
};

/** Empty cart for EmptyCart story variant - use cartId "storybook-cart-empty" */
const MOCK_EMPTY_CART = {
...MOCK_CART,
id: 'storybook-cart-empty',
items: {
data: [],
total: 0,
},
subtotal: { value: 0, currency: 'EUR' as const },
discountTotal: { value: 0, currency: 'EUR' as const },
taxTotal: { value: 0, currency: 'EUR' as const },
shippingTotal: { value: 0, currency: 'EUR' as const },
total: { value: 0, currency: 'EUR' as const },
billingAddress: undefined,
shippingAddress: undefined,
shippingMethod: undefined,
paymentMethod: undefined,
promotions: undefined,
};

const MOCK_SHIPPING_OPTIONS = {
data: [
{
id: 'standard',
name: 'Standard Shipping',
description: '3-5 business days',
total: { value: 0, currency: 'EUR' as const },
subtotal: { value: 0, currency: 'EUR' as const },
},
{
id: 'express',
name: 'Express Shipping',
description: '1-2 business days',
total: { value: 15, currency: 'EUR' as const },
subtotal: { value: 15, currency: 'EUR' as const },
},
],
total: 2,
};

const MOCK_PAYMENT_PROVIDERS = {
data: [
{
id: 'card',
name: 'Credit Card',
description: 'Pay with Visa, Mastercard',
},
{
id: 'blik',
name: 'BLIK',
description: 'Instant mobile payment',
},
],
};

const MOCK_CHECKOUT_SUMMARY = {
cart: MOCK_CART,
shippingAddress: MOCK_CART.shippingAddress,
billingAddress: MOCK_CART.billingAddress,
shippingMethod: MOCK_CART.shippingMethod,
paymentMethod: MOCK_CART.paymentMethod,
totals: {
subtotal: MOCK_CART.subtotal,
shipping: MOCK_CART.shippingTotal ?? { value: 0, currency: 'EUR' as const },
tax: MOCK_CART.taxTotal ?? { value: 0, currency: 'EUR' as const },
discount: MOCK_CART.discountTotal ?? { value: 0, currency: 'EUR' as const },
total: MOCK_CART.total,
},
notes: MOCK_CART.notes,
email: MOCK_CART.email,
};

const MOCK_PLACE_ORDER_RESPONSE = {
order: {
id: 'ord-storybook-1',
total: MOCK_CART.total,
currency: 'EUR',
status: 'PENDING',
paymentStatus: 'PENDING',
},
paymentRedirectUrl: undefined,
};

export {
MOCK_CART,
MOCK_EMPTY_CART,
MOCK_SHIPPING_OPTIONS,
MOCK_PAYMENT_PROVIDERS,
MOCK_CHECKOUT_SUMMARY,
MOCK_PLACE_ORDER_RESPONSE,
};
Loading
Loading