From 5d14af1324926f557de7bc39d951e087cf1e9d45 Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 31 Aug 2026 11:01:39 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E5=85=B1=E4=BA=AB=E6=95=B4?= =?UTF-8?q?=E8=A1=8C=E5=BC=80=E5=85=B3=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 PlatformToggleRow 共享实现与类型导出 保留 common 兼容出口并补充组件行为测试 --- .../src/components/PlatformToggleRow.test.tsx | 79 ++++++++++ .../src/components/PlatformToggleRow.tsx | 134 +++++++++++++++++ packages/shared/src/components/index.ts | 6 + src/components/common/PlatformToggleRow.tsx | 138 +----------------- 4 files changed, 224 insertions(+), 133 deletions(-) create mode 100644 packages/shared/src/components/PlatformToggleRow.test.tsx create mode 100644 packages/shared/src/components/PlatformToggleRow.tsx diff --git a/packages/shared/src/components/PlatformToggleRow.test.tsx b/packages/shared/src/components/PlatformToggleRow.test.tsx new file mode 100644 index 000000000..862721aa9 --- /dev/null +++ b/packages/shared/src/components/PlatformToggleRow.test.tsx @@ -0,0 +1,79 @@ +/* @vitest-environment jsdom */ + +import { fireEvent, render, screen } from '@testing-library/react'; +import { expect, test, vi } from 'vitest'; + +import { PlatformToggleRow } from './index'; + +test('renders a controlled checkbox row and forwards the next value', () => { + const onChange = vi.fn(); + render( + , + ); + + fireEvent.click(screen.getByRole('checkbox', { name: '自由输入' })); + + expect(onChange).toHaveBeenCalledWith(true); + expect(screen.getByText('自由输入').closest('label')?.className).toContain( + 'bg-white/74', + ); +}); + +test('renders read-only and clickable status variants with custom labels', () => { + const onClick = vi.fn(); + const { rerender } = render( + , + ); + + expect(screen.queryByRole('checkbox')).toBeNull(); + expect(screen.getByText('已开启')).toBeTruthy(); + expect(screen.getByText('文本模式').closest('div')?.className).toContain( + 'bg-white/78', + ); + + rerender( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '文本模式 未开启' })); + expect(onClick).toHaveBeenCalledTimes(1); +}); + +test('keeps disabled state and visual slots', () => { + render( + } + className="custom-row" + labelClassName="custom-label" + />, + ); + + expect(screen.getByRole('checkbox', { name: '玩家可见' })).toHaveProperty( + 'disabled', + true, + ); + expect(screen.getByTestId('toggle-icon')).toBeTruthy(); + expect(screen.getByText('玩家可见').closest('label')?.className).toContain( + 'custom-row', + ); + expect(screen.getByText('玩家可见').parentElement?.className).toContain( + 'custom-label', + ); +}); diff --git a/packages/shared/src/components/PlatformToggleRow.tsx b/packages/shared/src/components/PlatformToggleRow.tsx new file mode 100644 index 000000000..944d15b03 --- /dev/null +++ b/packages/shared/src/components/PlatformToggleRow.tsx @@ -0,0 +1,134 @@ +import type { ChangeEvent, ReactNode } from 'react'; + +import { getPlatformPillBadgeClassName } from './platformPillBadgeModel'; + +export type PlatformToggleRowSurface = 'soft' | 'plain'; +export type PlatformToggleRowMode = 'checkbox' | 'status'; + +export type PlatformToggleRowProps = { + label: ReactNode; + checked: boolean; + onChange?: (checked: boolean) => void; + disabled?: boolean; + mode?: PlatformToggleRowMode; + icon?: ReactNode; + onLabel?: ReactNode; + offLabel?: ReactNode; + onClick?: () => void; + className?: string; + labelClassName?: string; + surface?: PlatformToggleRowSurface; +}; + +const PLATFORM_TOGGLE_ROW_SURFACE_CLASS: Record< + PlatformToggleRowSurface, + string +> = { + soft: 'bg-white/74', + plain: 'bg-white/78', +}; + +function renderToggleStatus({ + checked, + offLabel, + onLabel, +}: { + checked: boolean; + offLabel: ReactNode; + onLabel: ReactNode; +}) { + return ( + + {checked ? onLabel : offLabel} + + ); +} + +/** + * 平台整行开关。 + * 统一承接设置面板和结果页配置里的白底 label + checkbox / 状态行。 + */ +export function PlatformToggleRow({ + label, + checked, + onChange, + disabled = false, + mode = 'checkbox', + icon, + onLabel = '开启', + offLabel = '关闭', + onClick, + className, + labelClassName, + surface = 'soft', +}: PlatformToggleRowProps) { + const handleChange = (event: ChangeEvent) => { + onChange?.(event.target.checked); + }; + + const rowClassName = [ + 'flex min-h-12 items-center justify-between gap-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] px-3', + PLATFORM_TOGGLE_ROW_SURFACE_CLASS[surface], + disabled ? 'opacity-60' : null, + className, + ] + .filter(Boolean) + .join(' '); + const labelNode = ( + + {icon ? {icon} : null} + {label} + + ); + + if (mode === 'status') { + if (onClick) { + return ( + + ); + } + + return ( +
+ {labelNode} + {renderToggleStatus({ checked, onLabel, offLabel })} +
+ ); + } + + return ( + + ); +} diff --git a/packages/shared/src/components/index.ts b/packages/shared/src/components/index.ts index 14d98e1ff..cac8cfd0b 100644 --- a/packages/shared/src/components/index.ts +++ b/packages/shared/src/components/index.ts @@ -122,6 +122,12 @@ export type { PlatformTextFieldProps, } from './PlatformTextField'; export { PlatformSelectField, PlatformTextField } from './PlatformTextField'; +export type { + PlatformToggleRowMode, + PlatformToggleRowProps, + PlatformToggleRowSurface, +} from './PlatformToggleRow'; +export { PlatformToggleRow } from './PlatformToggleRow'; export type { BadgeProps, BadgeTone, diff --git a/src/components/common/PlatformToggleRow.tsx b/src/components/common/PlatformToggleRow.tsx index 1eab276e1..4a6ab3168 100644 --- a/src/components/common/PlatformToggleRow.tsx +++ b/src/components/common/PlatformToggleRow.tsx @@ -1,134 +1,6 @@ -import type { ChangeEvent, ReactNode } from 'react'; - -import { getPlatformPillBadgeClassName } from './platformPillBadgeModel'; - -type PlatformToggleRowSurface = 'soft' | 'plain'; -type PlatformToggleRowMode = 'checkbox' | 'status'; - -type PlatformToggleRowProps = { - label: ReactNode; - checked: boolean; - onChange?: (checked: boolean) => void; - disabled?: boolean; - mode?: PlatformToggleRowMode; - icon?: ReactNode; - onLabel?: ReactNode; - offLabel?: ReactNode; - onClick?: () => void; - className?: string; - labelClassName?: string; - surface?: PlatformToggleRowSurface; -}; - -const PLATFORM_TOGGLE_ROW_SURFACE_CLASS: Record< +export type { + PlatformToggleRowMode, + PlatformToggleRowProps, PlatformToggleRowSurface, - string -> = { - soft: 'bg-white/74', - plain: 'bg-white/78', -}; - -function renderToggleStatus({ - checked, - offLabel, - onLabel, -}: { - checked: boolean; - offLabel: ReactNode; - onLabel: ReactNode; -}) { - return ( - - {checked ? onLabel : offLabel} - - ); -} - -/** - * 平台整行开关。 - * 统一承接设置面板和结果页配置里的白底 label + checkbox / 状态行。 - */ -export function PlatformToggleRow({ - label, - checked, - onChange, - disabled = false, - mode = 'checkbox', - icon, - onLabel = '开启', - offLabel = '关闭', - onClick, - className, - labelClassName, - surface = 'soft', -}: PlatformToggleRowProps) { - const handleChange = (event: ChangeEvent) => { - onChange?.(event.target.checked); - }; - - const rowClassName = [ - 'flex min-h-12 items-center justify-between gap-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] px-3', - PLATFORM_TOGGLE_ROW_SURFACE_CLASS[surface], - disabled ? 'opacity-60' : null, - className, - ] - .filter(Boolean) - .join(' '); - const labelNode = ( - - {icon ? {icon} : null} - {label} - - ); - - if (mode === 'status') { - if (onClick) { - return ( - - ); - } - - return ( -
- {labelNode} - {renderToggleStatus({ checked, onLabel, offLabel })} -
- ); - } - - return ( - - ); -} +} from '@genarrative/shared/components'; +export { PlatformToggleRow } from '@genarrative/shared/components';