From 55f6912734de9ac3ebad2ec75d3afadf3d0b070e Mon Sep 17 00:00:00 2001 From: Gaubee Date: Sun, 11 Jan 2026 20:39:37 +0800 Subject: [PATCH] feat(key-ui): add Storybook configuration and component stories - Add .storybook/main.ts, preview.tsx, vitest.setup.ts - Add AddressDisplay.stories.tsx with 8 story variants - Add AmountDisplay.stories.tsx with 9 story variants - Add TokenIcon.stories.tsx with 6 story variants - Add vitest.config.ts with unit and storybook projects - Update package.json with storybook dependencies and scripts All 23 Storybook tests passing. --- packages/key-ui/.storybook/main.ts | 12 ++ packages/key-ui/.storybook/preview.tsx | 84 ++++++++++++++ packages/key-ui/.storybook/vitest.setup.ts | 7 ++ packages/key-ui/package.json | 11 +- .../AddressDisplay.stories.tsx | 81 +++++++++++++ .../amount-display/AmountDisplay.stories.tsx | 106 ++++++++++++++++++ .../src/token-icon/TokenIcon.stories.tsx | 92 +++++++++++++++ packages/key-ui/vitest.config.ts | 45 ++++++++ pnpm-lock.yaml | 15 +++ 9 files changed, 451 insertions(+), 2 deletions(-) create mode 100644 packages/key-ui/.storybook/main.ts create mode 100644 packages/key-ui/.storybook/preview.tsx create mode 100644 packages/key-ui/.storybook/vitest.setup.ts create mode 100644 packages/key-ui/src/address-display/AddressDisplay.stories.tsx create mode 100644 packages/key-ui/src/amount-display/AmountDisplay.stories.tsx create mode 100644 packages/key-ui/src/token-icon/TokenIcon.stories.tsx create mode 100644 packages/key-ui/vitest.config.ts diff --git a/packages/key-ui/.storybook/main.ts b/packages/key-ui/.storybook/main.ts new file mode 100644 index 000000000..38735a63f --- /dev/null +++ b/packages/key-ui/.storybook/main.ts @@ -0,0 +1,12 @@ +import type { StorybookConfig } from '@storybook/react-vite' + +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: ['@storybook/addon-vitest', '@storybook/addon-docs'], + framework: { + name: '@storybook/react-vite', + options: {}, + }, +} + +export default config diff --git a/packages/key-ui/.storybook/preview.tsx b/packages/key-ui/.storybook/preview.tsx new file mode 100644 index 000000000..f02471439 --- /dev/null +++ b/packages/key-ui/.storybook/preview.tsx @@ -0,0 +1,84 @@ +import type { Preview, ReactRenderer } from '@storybook/react-vite' +import type { DecoratorFunction } from 'storybook/internal/types' +import { useEffect } from 'react' +import '../src/styles/base.css' + +const mobileViewports = { + iPhoneSE: { + name: 'iPhone SE', + styles: { width: '375px', height: '667px' }, + type: 'mobile' as const, + }, + iPhone13: { + name: 'iPhone 13', + styles: { width: '390px', height: '844px' }, + type: 'mobile' as const, + }, + iPhone13ProMax: { + name: 'iPhone 13 Pro Max', + styles: { width: '428px', height: '926px' }, + type: 'mobile' as const, + }, +} + +const preview: Preview = { + parameters: { + viewport: { + viewports: mobileViewports, + defaultViewport: 'iPhone13', + }, + backgrounds: { + disable: true, + }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, + globalTypes: { + theme: { + name: 'Theme', + description: 'Color theme', + defaultValue: 'light', + toolbar: { + icon: 'paintbrush', + items: [ + { value: 'light', title: 'Light', icon: 'sun' }, + { value: 'dark', title: 'Dark', icon: 'moon' }, + ], + dynamicTitle: true, + }, + }, + }, + decorators: [ + ((Story, context) => { + const theme = context.globals['theme'] || 'light' + + useEffect(() => { + if (theme === 'dark') { + document.documentElement.classList.add('dark') + } else { + document.documentElement.classList.remove('dark') + } + }, [theme]) + + return ( +
+
+ +
+
+ ) + }) as DecoratorFunction, + ], +} + +export default preview diff --git a/packages/key-ui/.storybook/vitest.setup.ts b/packages/key-ui/.storybook/vitest.setup.ts new file mode 100644 index 000000000..73b806072 --- /dev/null +++ b/packages/key-ui/.storybook/vitest.setup.ts @@ -0,0 +1,7 @@ +import { beforeAll } from 'vitest' +import { setProjectAnnotations } from '@storybook/react-vite' +import * as previewAnnotations from './preview' + +const annotations = setProjectAnnotations([previewAnnotations]) + +beforeAll(annotations.beforeAll) diff --git a/packages/key-ui/package.json b/packages/key-ui/package.json index 33cb30214..954dca3d5 100644 --- a/packages/key-ui/package.json +++ b/packages/key-ui/package.json @@ -39,8 +39,10 @@ "typecheck": "tsc --noEmit", "typecheck:run": "tsc --noEmit", "test": "vitest", - "test:run": "vitest run --passWithNoTests", - "test:storybook": "echo 'No storybook'", + "test:run": "vitest run --project unit --passWithNoTests", + "test:storybook": "vitest run --project storybook --passWithNoTests", + "storybook": "storybook dev -p 6008", + "storybook:build": "storybook build", "lint:run": "oxlint .", "i18n:run": "echo 'No i18n'", "theme:run": "echo 'No theme'" @@ -54,13 +56,18 @@ "react-dom": "^19.0.0" }, "devDependencies": { + "@storybook/addon-docs": "^10.0.0", + "@storybook/addon-vitest": "^10.0.0", + "@storybook/react-vite": "^10.0.0", "@testing-library/react": "^16.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", + "@vitest/browser-playwright": "^4.0.0", "clsx": "^2.1.1", "eslint-plugin-file-component-constraints": "workspace:*", "jsdom": "^26.1.0", "oxlint": "^1.32.0", + "storybook": "^10.0.0", "tailwind-merge": "^3.4.0", "typescript": "^5.9.3", "vite": "^7.3.0", diff --git a/packages/key-ui/src/address-display/AddressDisplay.stories.tsx b/packages/key-ui/src/address-display/AddressDisplay.stories.tsx new file mode 100644 index 000000000..0a4351c38 --- /dev/null +++ b/packages/key-ui/src/address-display/AddressDisplay.stories.tsx @@ -0,0 +1,81 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { AddressDisplay } from './AddressDisplay' + +const meta: Meta = { + title: 'Components/AddressDisplay', + component: AddressDisplay, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + mode: { + control: 'select', + options: ['auto', 'compact', 'standard', 'detailed', 'full'], + }, + copyable: { + control: 'boolean', + }, + }, +} + +export default meta +type Story = StoryObj + +const SAMPLE_ADDRESS = '0x1234567890abcdef1234567890abcdef12345678' + +export const Default: Story = { + args: { + address: SAMPLE_ADDRESS, + }, +} + +export const Compact: Story = { + args: { + address: SAMPLE_ADDRESS, + mode: 'compact', + }, +} + +export const Standard: Story = { + args: { + address: SAMPLE_ADDRESS, + mode: 'standard', + }, +} + +export const Detailed: Story = { + args: { + address: SAMPLE_ADDRESS, + mode: 'detailed', + }, +} + +export const Full: Story = { + args: { + address: SAMPLE_ADDRESS, + mode: 'full', + }, +} + +export const NotCopyable: Story = { + args: { + address: SAMPLE_ADDRESS, + copyable: false, + }, +} + +export const CustomChars: Story = { + args: { + address: SAMPLE_ADDRESS, + startChars: 10, + endChars: 8, + }, +} + +export const Empty: Story = { + args: { + address: '', + placeholder: 'No address', + }, +} diff --git a/packages/key-ui/src/amount-display/AmountDisplay.stories.tsx b/packages/key-ui/src/amount-display/AmountDisplay.stories.tsx new file mode 100644 index 000000000..8b8718379 --- /dev/null +++ b/packages/key-ui/src/amount-display/AmountDisplay.stories.tsx @@ -0,0 +1,106 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { AmountDisplay } from './AmountDisplay' + +const meta: Meta = { + title: 'Components/AmountDisplay', + component: AmountDisplay, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + sign: { + control: 'select', + options: ['auto', 'always', 'never'], + }, + color: { + control: 'select', + options: ['auto', 'default', 'positive', 'negative'], + }, + size: { + control: 'select', + options: ['xs', 'sm', 'md', 'lg', 'xl'], + }, + weight: { + control: 'select', + options: ['normal', 'medium', 'semibold', 'bold'], + }, + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + value: '1234.56789', + symbol: 'ETH', + }, +} + +export const Positive: Story = { + args: { + value: '1234.56789', + symbol: 'ETH', + sign: 'always', + color: 'auto', + }, +} + +export const Negative: Story = { + args: { + value: '-1234.56789', + symbol: 'ETH', + sign: 'auto', + color: 'auto', + }, +} + +export const Compact: Story = { + args: { + value: '1234567.89', + symbol: 'ETH', + compact: true, + }, +} + +export const Large: Story = { + args: { + value: '1234.56789', + symbol: 'ETH', + size: 'xl', + weight: 'bold', + }, +} + +export const Loading: Story = { + args: { + value: '0', + symbol: 'ETH', + loading: true, + }, +} + +export const Hidden: Story = { + args: { + value: '1234.56789', + symbol: 'ETH', + hidden: true, + }, +} + +export const NoAnimation: Story = { + args: { + value: '1234.56789', + symbol: 'ETH', + animated: false, + }, +} + +export const HighDecimals: Story = { + args: { + value: '0.00000001', + symbol: 'BTC', + decimals: 8, + }, +} diff --git a/packages/key-ui/src/token-icon/TokenIcon.stories.tsx b/packages/key-ui/src/token-icon/TokenIcon.stories.tsx new file mode 100644 index 000000000..68e520081 --- /dev/null +++ b/packages/key-ui/src/token-icon/TokenIcon.stories.tsx @@ -0,0 +1,92 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { TokenIcon } from './index' + +const meta: Meta = { + title: 'Components/TokenIcon', + component: TokenIcon.Root, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + size: { + control: 'select', + options: ['xs', 'sm', 'md', 'lg', 'xl'], + }, + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + size: 'md', + }, + render: (args) => ( + + + ETH + + ), +} + +export const Small: Story = { + args: { + size: 'sm', + }, + render: (args) => ( + + + BTC + + ), +} + +export const Large: Story = { + args: { + size: 'xl', + }, + render: (args) => ( + + + SOL + + ), +} + +export const Fallback: Story = { + args: { + size: 'md', + }, + render: (args) => ( + + + ?? + + ), +} + +export const NoImage: Story = { + args: { + size: 'lg', + }, + render: (args) => ( + + ABC + + ), +} + +export const AllSizes: Story = { + render: () => ( +
+ {(['xs', 'sm', 'md', 'lg', 'xl'] as const).map((size) => ( + + + ETH + + ))} +
+ ), +} diff --git a/packages/key-ui/vitest.config.ts b/packages/key-ui/vitest.config.ts new file mode 100644 index 000000000..04fa357a4 --- /dev/null +++ b/packages/key-ui/vitest.config.ts @@ -0,0 +1,45 @@ +import { defineConfig } from 'vitest/config' +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin' +import { playwright } from '@vitest/browser-playwright' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const dirname = path.dirname(fileURLToPath(import.meta.url)) + +export default defineConfig({ + test: { + projects: [ + // Unit tests (jsdom) + { + extends: './vite.config.ts', + test: { + name: 'unit', + globals: true, + environment: 'jsdom', + include: ['src/**/*.test.{ts,tsx}'], + exclude: ['src/**/*.stories.test.{ts,tsx}'], + }, + }, + // Storybook component tests (real browser) + { + extends: './vite.config.ts', + plugins: [ + storybookTest({ + configDir: path.join(dirname, '.storybook'), + storybookScript: 'pnpm storybook --ci', + }), + ], + test: { + name: 'storybook', + browser: { + enabled: true, + provider: playwright(), + headless: true, + instances: [{ browser: 'chromium' }], + }, + setupFiles: ['./.storybook/vitest.setup.ts'], + }, + }, + ], + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8407f6c6e..82899a25d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -741,6 +741,15 @@ importers: specifier: ^19.0.0 version: 19.2.3(react@19.2.3) devDependencies: + '@storybook/addon-docs': + specifier: ^10.0.0 + version: 10.1.10(@types/react@19.2.7)(esbuild@0.27.2)(rollup@4.54.0)(storybook@10.1.10(@testing-library/dom@10.4.0)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)) + '@storybook/addon-vitest': + specifier: ^10.0.0 + version: 10.1.10(@vitest/browser-playwright@4.0.16)(@vitest/browser@4.0.16(msw@2.12.4(@types/node@24.10.4)(typescript@5.9.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2))(vitest@4.0.16))(@vitest/runner@4.0.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.10(@testing-library/dom@10.4.0)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vitest@4.0.16) + '@storybook/react-vite': + specifier: ^10.0.0 + version: 10.1.10(esbuild@0.27.2)(msw@2.12.4(@types/node@24.10.4)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.54.0)(storybook@10.1.10(@testing-library/dom@10.4.0)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)) '@testing-library/react': specifier: ^16.0.0 version: 16.3.1(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -750,6 +759,9 @@ importers: '@types/react-dom': specifier: ^19.0.0 version: 19.2.3(@types/react@19.2.7) + '@vitest/browser-playwright': + specifier: ^4.0.0 + version: 4.0.16(msw@2.12.4(@types/node@24.10.4)(typescript@5.9.3))(playwright@1.57.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2))(vitest@4.0.16) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -762,6 +774,9 @@ importers: oxlint: specifier: ^1.32.0 version: 1.35.0 + storybook: + specifier: ^10.0.0 + version: 10.1.10(@testing-library/dom@10.4.0)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tailwind-merge: specifier: ^3.4.0 version: 3.4.0