Skip to content

Commit f1cfa0f

Browse files
committed
feat: storybook update
1 parent dcb60d7 commit f1cfa0f

File tree

15 files changed

+841
-326
lines changed

15 files changed

+841
-326
lines changed

.storybook/main.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,59 @@ const config: StorybookConfig = {
3636

3737
// add Less and Less Module support for project styles
3838
config.module = config.module || { rules: [] }
39+
40+
const excludeProjectStyleFromDefaultRules = (rules: any[]) => {
41+
const visit = (rule: any) => {
42+
if (!rule) return
43+
if (Array.isArray(rule.oneOf)) rule.oneOf.forEach(visit)
44+
if (Array.isArray(rule.rules)) rule.rules.forEach(visit)
45+
46+
const test = rule.test
47+
const testStr = typeof test?.toString === 'function' ? test.toString() : ''
48+
49+
// Storybook may have implicit style loaders. Exclude project-handled patterns
50+
// to prevent double-processing (which can break CSS Modules output).
51+
if (testStr.includes('\\.css')) {
52+
const moduleCss = /\.module\.css$/
53+
if (!rule.exclude) {
54+
rule.exclude = moduleCss
55+
} else if (Array.isArray(rule.exclude)) {
56+
rule.exclude = [...rule.exclude, moduleCss]
57+
} else {
58+
rule.exclude = [rule.exclude, moduleCss]
59+
}
60+
}
61+
62+
if (testStr.includes('\\.less')) {
63+
const allLess = /\.less$/
64+
if (!rule.exclude) {
65+
rule.exclude = allLess
66+
} else if (Array.isArray(rule.exclude)) {
67+
rule.exclude = [...rule.exclude, allLess]
68+
} else {
69+
rule.exclude = [rule.exclude, allLess]
70+
}
71+
}
72+
}
73+
74+
rules.forEach(visit)
75+
}
76+
77+
const cssModuleRule = {
78+
test: /\.module\.css$/,
79+
use: [
80+
'style-loader',
81+
{
82+
loader: 'css-loader',
83+
options: {
84+
modules: { localIdentName: '[local]__[hash:base64:5]' },
85+
importLoaders: 1,
86+
},
87+
},
88+
'postcss-loader',
89+
],
90+
}
91+
3992
const lessModuleRule = {
4093
test: /\.module\.less$/,
4194
use: [
@@ -56,7 +109,8 @@ const config: StorybookConfig = {
56109
}
57110

58111
const lessRule = {
59-
test: /(?<!\.module)\.less$/,
112+
test: /\.less$/,
113+
exclude: /\.module\.less$/,
60114
use: [
61115
'style-loader',
62116
'css-loader',
@@ -68,8 +122,14 @@ const config: StorybookConfig = {
68122
],
69123
}
70124

125+
// Prevent Storybook's default style rules from also processing project-handled patterns.
126+
// IMPORTANT: only apply these excludes to the existing/default rules, not the custom rules
127+
// we prepend below.
128+
const baseRules = config.module.rules || []
129+
excludeProjectStyleFromDefaultRules(baseRules)
130+
71131
// prepend to ensure project rules take precedence
72-
config.module.rules = [lessModuleRule, lessRule, ...(config.module.rules || [])]
132+
config.module.rules = [cssModuleRule, lessModuleRule, lessRule, ...baseRules]
73133

74134
return config
75135
},

.storybook/preview.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
import React from 'react'
1+
import React, { Suspense } from 'react'
22
import type { Preview } from '@storybook/react-webpack5'
33
import { MemoryRouter } from 'react-router-dom'
4+
import { I18nextProvider } from 'react-i18next'
5+
6+
import i18n from '../src/i18n/i18n'
47

58
const preview: Preview = {
69
decorators: [
710
(Story) => (
8-
<MemoryRouter initialEntries={['/']}>
9-
<Story />
10-
</MemoryRouter>
11+
<I18nextProvider i18n={i18n}>
12+
<Suspense fallback={<div style={{ padding: 12 }}>Loading…</div>}>
13+
<MemoryRouter initialEntries={['/']}>
14+
<Story />
15+
</MemoryRouter>
16+
</Suspense>
17+
</I18nextProvider>
1118
),
1219
],
1320
parameters: {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
title: OneTimePasscode
3+
---
4+
5+
# OneTimePasscode
6+
7+
用于输入一次性验证码(OTP / PIN)的轻量输入组件,支持:
8+
9+
- 受控/非受控
10+
- IME 合成输入
11+
- 粘贴一串字符自动分配
12+
- 三种视觉变体(`modern` / `compact` / `classic`
13+
14+
组件样式使用 CSS Modules(`index.module.less`)。
15+
16+
## 基本使用
17+
18+
### 受控用法
19+
20+
```tsx
21+
import React, { useState } from 'react'
22+
import OneTimePasscode from '@stateless/OneTimePasscode'
23+
24+
export default function Example() {
25+
const [code, setCode] = useState('')
26+
return <OneTimePasscode length={6} value={code} onChange={setCode} autoFocus variant="modern" />
27+
}
28+
```
29+
30+
### 非受控用法
31+
32+
```tsx
33+
import OneTimePasscode from '@stateless/OneTimePasscode'
34+
35+
export default function Example() {
36+
return <OneTimePasscode length={4} onChange={(v) => console.log('value', v)} />
37+
}
38+
```
39+
40+
## 常用属性
41+
42+
- `length`: 验证码长度(默认 6)
43+
- `value` / `onChange`: 传入 `value` 即为受控
44+
- `variant`: `modern | compact | classic`
45+
- `validateChar`: 自定义字符校验(例如只允许数字)
46+
47+
示例:只允许数字
48+
49+
```tsx
50+
<OneTimePasscode length={6} validateChar={(ch) => ch === '' || /\d/.test(ch)} />
51+
```
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// @ts-ignore: storybook types resolution can be environment-specific in CI/TS config
2+
import type { Meta, StoryObj } from '@storybook/react-webpack5'
3+
import React, { useState } from 'react'
4+
5+
import OneTimePasscode from '@stateless/OneTimePasscode'
6+
7+
const Shell = ({ children }: { children: React.ReactNode }) => (
8+
<div style={{ padding: 24, minWidth: 320 }}>{children}</div>
9+
)
10+
11+
const meta = {
12+
title: 'ProReactAdmin/OneTimePasscode',
13+
component: OneTimePasscode,
14+
parameters: {
15+
layout: 'centered',
16+
},
17+
args: {
18+
length: 6,
19+
variant: 'modern',
20+
autoFocus: false,
21+
disabled: false,
22+
},
23+
argTypes: {
24+
variant: {
25+
control: { type: 'select' },
26+
options: ['modern', 'compact', 'classic'],
27+
},
28+
},
29+
} satisfies Meta<typeof OneTimePasscode>
30+
31+
export default meta
32+
33+
type Story = StoryObj<typeof OneTimePasscode>
34+
35+
export const Modern: Story = {
36+
render: (args) => (
37+
<Shell>
38+
<OneTimePasscode {...args} variant="modern" />
39+
</Shell>
40+
),
41+
}
42+
43+
export const Compact: Story = {
44+
render: (args) => (
45+
<Shell>
46+
<OneTimePasscode {...args} variant="compact" />
47+
</Shell>
48+
),
49+
}
50+
51+
export const Classic: Story = {
52+
render: (args) => (
53+
<Shell>
54+
<OneTimePasscode {...args} variant="classic" />
55+
</Shell>
56+
),
57+
}
58+
59+
export const Controlled: Story = {
60+
render: (args) => {
61+
const [val, setVal] = useState('')
62+
return (
63+
<Shell>
64+
<OneTimePasscode {...args} value={val} onChange={(v) => setVal(v)} />
65+
</Shell>
66+
)
67+
},
68+
}
69+
70+
export const Uncontrolled: Story = {
71+
render: (args) => (
72+
<Shell>
73+
<OneTimePasscode {...args} />
74+
</Shell>
75+
),
76+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
title: SmartVideoPlayer
3+
---
4+
5+
# SmartVideoPlayer
6+
7+
统一的播放器封装(HTML5 / YouTube / Embed),内置设置面板、字幕/倍速、HLS(原生优先 + hls.js 动态加载)等能力。
8+
9+
- “界面语言”只影响 SmartVideoPlayer 自身(不影响全站 i18n)
10+
- 语言通过 `localStorage: svpUiLanguage` 持久化(刷新后仍生效)
11+
12+
## 基本使用
13+
14+
### HTML5(MP4)
15+
16+
```tsx
17+
import SmartVideoPlayer from '@stateless/SmartVideoPlayer'
18+
19+
export default function Example() {
20+
return (
21+
<SmartVideoPlayer
22+
provider="html5"
23+
title="HTML5 MP4"
24+
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
25+
/>
26+
)
27+
}
28+
```
29+
30+
### YouTube
31+
32+
```tsx
33+
import SmartVideoPlayer from '@stateless/SmartVideoPlayer'
34+
35+
export default function Example() {
36+
return <SmartVideoPlayer provider="youtube" title="YouTube" youtubeId="dQw4w9WgXcQ" />
37+
}
38+
```
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// @ts-ignore: storybook types resolution can be environment-specific in CI/TS config
2+
import type { Meta, StoryObj } from '@storybook/react-webpack5'
3+
import React from 'react'
4+
import type { ComponentType } from 'react'
5+
6+
import SmartVideoPlayer from '@stateless/SmartVideoPlayer'
7+
8+
// SmartVideoPlayer is implemented in JS via forwardRef/memo, so TS only infers RefAttributes.
9+
// Cast to a generic component type for Storybook args/controls.
10+
const SmartVideoPlayerAny = SmartVideoPlayer as unknown as ComponentType<any>
11+
12+
const meta = {
13+
title: 'ProReactAdmin/SmartVideoPlayer',
14+
component: SmartVideoPlayerAny,
15+
parameters: {
16+
layout: 'fullscreen',
17+
},
18+
args: {
19+
title: 'Demo',
20+
provider: 'html5',
21+
src: '',
22+
youtubeId: 'dQw4w9WgXcQ',
23+
embedUrl: '',
24+
externalUrl: '',
25+
sourceUrl: '',
26+
},
27+
argTypes: {
28+
provider: {
29+
control: { type: 'select' },
30+
options: ['html5', 'youtube', 'embed'],
31+
},
32+
},
33+
render: (args) => {
34+
const { provider } = args
35+
36+
// Keep a fixed demo shell similar to the video page.
37+
return (
38+
<div style={{ padding: 16, maxWidth: 980, margin: '0 auto' }}>
39+
<SmartVideoPlayer
40+
{...args}
41+
provider={provider}
42+
// Avoid passing invalid props for a provider.
43+
src={provider === 'html5' ? args.src : undefined}
44+
youtubeId={provider === 'youtube' ? args.youtubeId : undefined}
45+
embedUrl={provider === 'embed' ? args.embedUrl : undefined}
46+
/>
47+
</div>
48+
)
49+
},
50+
} satisfies Meta<typeof SmartVideoPlayerAny>
51+
52+
export default meta
53+
54+
type Story = StoryObj<typeof SmartVideoPlayerAny>
55+
56+
export const Html5Mp4: Story = {
57+
args: {
58+
provider: 'html5',
59+
src: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
60+
title: 'HTML5 MP4',
61+
sourceUrl: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
62+
},
63+
}
64+
65+
export const Html5Hls: Story = {
66+
args: {
67+
provider: 'html5',
68+
// Public demo stream.
69+
src: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8',
70+
title: 'HTML5 HLS',
71+
sourceUrl: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8',
72+
},
73+
}
74+
75+
export const YouTube: Story = {
76+
args: {
77+
provider: 'youtube',
78+
youtubeId: 'dQw4w9WgXcQ',
79+
title: 'YouTube',
80+
},
81+
}
82+
83+
export const Embed: Story = {
84+
args: {
85+
provider: 'embed',
86+
// A simple embed example.
87+
embedUrl: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
88+
title: 'Embed',
89+
},
90+
}

0 commit comments

Comments
 (0)