继续收口暗色弹窗底部动作区
新增 PlatformDarkModalFooter 统一 dark modal footer 的布局壳 接入 NPC 弹窗、选择定制弹窗、任务更新弹层与物品详情 footer 补充组件级与弹窗集成测试并更新收口计划和共享决策记录
This commit is contained in:
65
src/components/common/PlatformDarkModalFooter.test.tsx
Normal file
65
src/components/common/PlatformDarkModalFooter.test.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { PlatformDarkModalFooter } from './PlatformDarkModalFooter';
|
||||
|
||||
describe('PlatformDarkModalFooter', () => {
|
||||
test('renders bordered action footer with shared action row chrome', () => {
|
||||
render(
|
||||
<PlatformDarkModalFooter data-testid="footer">
|
||||
<button type="button">取消</button>
|
||||
<button type="button">确认</button>
|
||||
</PlatformDarkModalFooter>,
|
||||
);
|
||||
|
||||
const footer = screen.getByTestId('footer');
|
||||
const actions = footer.querySelector('.platform-dark-modal-footer__actions');
|
||||
|
||||
expect(footer.className).toContain('platform-dark-modal-footer');
|
||||
expect(footer.className).toContain('border-t');
|
||||
expect(footer.className).toContain('border-white/10');
|
||||
expect(footer.className).toContain('px-4');
|
||||
expect(footer.className).toContain('py-3');
|
||||
expect(actions?.className).toContain('justify-end');
|
||||
expect(actions?.className).toContain('gap-3');
|
||||
});
|
||||
|
||||
test('supports unbordered bottom padding actions for modal footer tails', () => {
|
||||
render(
|
||||
<PlatformDarkModalFooter
|
||||
bordered={false}
|
||||
padding="bottom"
|
||||
gap="sm"
|
||||
data-testid="footer"
|
||||
>
|
||||
<button type="button">关闭</button>
|
||||
</PlatformDarkModalFooter>,
|
||||
);
|
||||
|
||||
const footer = screen.getByTestId('footer');
|
||||
const actions = footer.querySelector('.platform-dark-modal-footer__actions');
|
||||
|
||||
expect(footer.className).not.toContain('border-t');
|
||||
expect(footer.className).toContain('px-5');
|
||||
expect(footer.className).toContain('pb-5');
|
||||
expect(actions?.className).toContain('gap-2');
|
||||
});
|
||||
|
||||
test('supports content layout without wrapping children in an actions row', () => {
|
||||
render(
|
||||
<PlatformDarkModalFooter layout="content" data-testid="footer">
|
||||
<div data-testid="content">自定义内容</div>
|
||||
</PlatformDarkModalFooter>,
|
||||
);
|
||||
|
||||
const footer = screen.getByTestId('footer');
|
||||
|
||||
expect(screen.getByTestId('content')).toBeTruthy();
|
||||
expect(
|
||||
footer.querySelector('.platform-dark-modal-footer__actions'),
|
||||
).toBeNull();
|
||||
expect(footer.className).toContain('platform-dark-modal-footer');
|
||||
});
|
||||
});
|
||||
86
src/components/common/PlatformDarkModalFooter.tsx
Normal file
86
src/components/common/PlatformDarkModalFooter.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
|
||||
type PlatformDarkModalFooterLayout = 'actions' | 'content';
|
||||
type PlatformDarkModalFooterPadding = 'compact' | 'roomy' | 'bottom';
|
||||
type PlatformDarkModalFooterGap = 'sm' | 'md';
|
||||
type PlatformDarkModalFooterAlign = 'start' | 'center' | 'end' | 'between';
|
||||
|
||||
type PlatformDarkModalFooterProps = ComponentPropsWithoutRef<'div'> & {
|
||||
children: ReactNode;
|
||||
bordered?: boolean;
|
||||
layout?: PlatformDarkModalFooterLayout;
|
||||
padding?: PlatformDarkModalFooterPadding;
|
||||
gap?: PlatformDarkModalFooterGap;
|
||||
wrap?: boolean;
|
||||
align?: PlatformDarkModalFooterAlign;
|
||||
contentClassName?: string;
|
||||
};
|
||||
|
||||
const PADDING_CLASS: Record<PlatformDarkModalFooterPadding, string> = {
|
||||
compact: 'px-4 py-3 sm:px-5 sm:py-4',
|
||||
roomy: 'px-5 py-4',
|
||||
bottom: 'px-5 pb-5',
|
||||
};
|
||||
|
||||
const GAP_CLASS: Record<PlatformDarkModalFooterGap, string> = {
|
||||
sm: 'gap-2',
|
||||
md: 'gap-3',
|
||||
};
|
||||
|
||||
const ALIGN_CLASS: Record<PlatformDarkModalFooterAlign, string> = {
|
||||
start: 'justify-start',
|
||||
center: 'justify-center',
|
||||
end: 'justify-end',
|
||||
between: 'justify-between',
|
||||
};
|
||||
|
||||
/**
|
||||
* 暗色 / 像素弹层底部 footer 骨架。
|
||||
* 统一承接 border、padding 和常见的动作按钮排布,避免业务页重复手写同一套 chrome。
|
||||
*/
|
||||
export function PlatformDarkModalFooter({
|
||||
children,
|
||||
bordered = true,
|
||||
layout = 'actions',
|
||||
padding = 'compact',
|
||||
gap = 'md',
|
||||
wrap = false,
|
||||
align = 'end',
|
||||
className,
|
||||
contentClassName,
|
||||
...props
|
||||
}: PlatformDarkModalFooterProps) {
|
||||
const frameClassName = [
|
||||
'platform-dark-modal-footer',
|
||||
bordered ? 'border-t border-white/10' : null,
|
||||
PADDING_CLASS[padding],
|
||||
className ?? null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
if (layout === 'content') {
|
||||
return (
|
||||
<div className={frameClassName} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const actionsClassName = [
|
||||
'platform-dark-modal-footer__actions',
|
||||
'flex items-center',
|
||||
ALIGN_CLASS[align],
|
||||
GAP_CLASS[gap],
|
||||
wrap ? 'flex-wrap' : null,
|
||||
contentClassName ?? null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<div className={frameClassName} {...props}>
|
||||
<div className={actionsClassName}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user