收口首页与详情动作按钮

为 CopyFeedbackButton 增加 actionShape 共享能力
将拼图广场详情 hero 动作迁移到共享按钮组件
将智能创作首页与抽屉入口迁移到共享按钮组件
将绑定手机号身份提示块迁移到 PlatformSubpanel
同步更新 PlatformUiKit 收口文档与团队决策记录
This commit is contained in:
2026-06-10 15:57:58 +08:00
parent 7411b9a435
commit eb73ffb34d
10 changed files with 163 additions and 53 deletions

View File

@@ -107,6 +107,9 @@
- 2026-06-10 追加:平台作品详情页主题标签使用 `PlatformPillBadge tone="neutralSolid" size="sm"`,作品号复制按钮使用 `CopyCodeButton actionAppearance="pill" actionPillTone="neutralSolid" actionPillSize="sm"`;详情页只保留标签映射、作品号复制状态和顶部外边距,不再手写 `platform-work-detail__chip / code` 基础 chrome。验证命令`npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/common/PlatformPillBadge.test.tsx src/components/common/CopyCodeButton.test.tsx`
- 2026-06-10 追加:平台作品详情页分享复制反馈使用 `PlatformStatusMessage surface="platform"`,按 `shareState` 映射 `success / error`;详情页保留 `useCopyFeedback` 状态机和文案,不再让失败态复用成功 toast chrome。验证命令`npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/common/PlatformStatusMessage.test.tsx`
- 2026-06-10 追加:平台错误弹窗和生成完成弹窗的“字段展示 + 复制整段报告”能力统一收口到 `src/components/common/PlatformReportDialog.tsx``PlatformErrorDialog``PlatformTaskCompletionDialog` 只保留标题、字段语义和错误黑名单过滤,不再各自组合 `UnifiedModal``PlatformInfoBlock``CopyFeedbackButton``useCopyFeedback`。验证命令:`npm run test -- src/components/common/PlatformReportDialog.test.tsx src/components/platform-entry/PlatformErrorDialog.test.tsx src/components/platform-entry/PlatformTaskCompletionDialog.test.tsx`
- 2026-06-10 追加:`CopyFeedbackButton` 支持 `actionShape`,用于共享复制状态按钮直接对齐 `PlatformActionButton` 的圆角外观;拼图广场详情页 hero 的分享按钮已使用 `actionSurface="editorDark" actionShape="pill"`,修改作品 / 进入第 1 关动作使用 `PlatformActionButton`,返回和封面轮播前后按钮使用 `PlatformIconButton darkMini`。验证命令:`npm run test -- src/components/common/CopyFeedbackButton.test.tsx src/components/puzzle-gallery/PuzzleGalleryDetailView.test.tsx`
- 2026-06-10 追加creative-agent 首页的侧边栏菜单、账号入口、开启新对话、我的创作和首页激励 CTA 迁移到 `PlatformIconButton` / `PlatformActionButton`,但继续保留 `creative-agent-home__*` 本地 class 承接透明顶栏和抽屉品牌视觉;收口按钮语义时不强行同时抹平定制视觉。验证命令:`npm run test -- src/components/creative-agent/CreativeAgentHome.test.tsx`
- 2026-06-10 追加:绑定手机号页左侧“当前登录身份”提示块迁移到 `PlatformSubpanel radius="sm" padding="md"`;认证页只保留身份文案和绑定流程,不再手写 `platform-subpanel` 信息块壳。验证命令:`npm run test -- src/components/auth/BindPhoneScreen.test.tsx`
- 2026-06-10 追加:`PlatformPillBadge` 支持 `darkSoft` / `darkNeutral` / `darkSky` / `darkEmerald` / `darkAmber` / `darkRose` 暗色 tone用于 RPG 暗色弹窗和角色详情里的纯展示 chip角色身份 / 等级、技能列表出手方式、技能详情方式 / 风格 / 状态标签、地图节点方向标签、地图场景切换方向标签和营地编组状态数值已迁移。暗色动作按钮、runtime HUD、属性加成动态 pill 和按钮内部消耗 chip 暂不直接套静态 badge。
- 2026-06-10 追加:背景故事已解锁 / 需好感状态和好感等级 badge 也使用 `PlatformPillBadge``dark*` tone好感进度时间轴刻度、runtime HUD 和带点击卡片视觉的标签仍保留专用布局。
- 2026-06-10 追加RPG 角色资产工作室动作列表的生成中 / 已生成 / 待生成状态 chip 直接使用 `PlatformPillBadge``darkAmber` / `darkEmerald` / `darkNeutral` tone父弹窗不再维护本地 `StatusBadge` 浅封装,动作生成按钮仍保留工作室专用暗色按钮布局。

View File

@@ -213,8 +213,12 @@
18.7.11. 发布分享弹窗渠道 tile 按钮迁移到 `PlatformSubpanel as="button" interactive surface="flat"`;复制反馈状态、渠道枚举和品牌图标继续留在分享弹窗内。
18.7.12. 平台入口创作类型弹层玩法卡片迁移到 `PlatformSubpanel as="button" surface="platform" radius="xl" padding="none"`;玩法图片蒙版、锁定 badge、标题副标题和分流回调继续由弹层组件持有。
18.7.13. creation-agent 工作台聊天区外壳迁移到 `PlatformSubpanel radius="xl" padding="none"`;消息列表、上传预览、错误提示和输入区继续由工作台组件持有。
18.7.14. 绑定手机号页左侧的“当前登录身份”提示块迁移到 `PlatformSubpanel as="div" radius="sm" padding="md"`;认证页只保留品牌说明、当前用户显示名和绑定流程,不再手写 `platform-subpanel` 信息块外壳。
18.8. 平台标签编辑器迁移到 `PlatformTagEditor`;拼图、敲木鱼和抓大鹅结果页标签编辑已先迁移。后续标签编辑只把 parse / normalize 和保存语义留在业务页,新增输入状态、删除 chip、空态、AI 生成按钮和错误提示统一由 Module 承接。
19. 个人中心充值、任务、兑换、邀请、支付结果等弹窗里的普通主动作按钮迁移到 `PlatformActionButton surface="profile"`RPG 首页作品卡删除小动作、RPG 作品详情、RPG / 拼图 / 抓大鹅 / 跳一跳 / 敲木鱼 / 拼消消 / 宝贝识物 / 方洞 / 汪汪声浪 / 视觉小说 / 大鱼吃小鱼结果页、自定义世界实体目录小动作、生成结果恢复面板、通用生成页重试 / 中断动作、法律信息弹窗 footer、公共确认弹窗 footer、统一创作工作台、统一创作页壳层、拼图创作工作台、拼消消创作工作台、宝贝识物创作工作台、视觉小说创作工作台、汪汪声浪创作工作台、creation-agent 推荐回复、creative-agent 工作台、creative-agent 模板确认弹窗、创作中心错误重试、创作中心作品卡积分激励领取按钮、反馈页 header 返回、通用创作输入面板、认证表单、敲木鱼 fallback 返回、跳一跳结算、拼消消 runtime header / 结算弹窗和视觉小说 runtime 普通白底面板里的普通主动作 / 次动作 / 危险动作迁移到 `PlatformActionButton surface="platform"`RPG 暗色弹窗 / 运行面板中的角色自定义 footer、生成 footer、地图切换确认、营地编组普通动作和角色聊天刷新动作迁移到 `PlatformActionButton surface="editorDark"`RPG 大编辑器暗色面板内的保存 / 角色槽动作继续走本地 `ActionButton`,不再混用白底平台 `platform-button` class。统一创作工作台、统一创作页壳层、玩法创作工作台、结果页返回按钮和反馈页 header 返回使用 `tone="ghost"`,提交 / 生成 / 发布 / 保存按钮使用默认主动作,素材槽小按钮、作品卡角落小动作、拼图图片生成模式选择器触发器和白底面板行内动作使用 `size="xs"``shape="pill"`,积分激励领取这类密集卡片小动作使用 `size="xxs"` 并由局部卡片 class 保留响应式布局,暗色微型刷新动作使用 `size="xxs" shape="pill"`,左对齐回复 / 列表动作使用 `align="start"`,认证表单提交、验证码、第三方登录和邀请码提交按钮使用 `size="lg"` 保持 48px 高度,文件上传 label 使用 `asChild="label"` 保持上传语义;复制邀请、错误复制、完成复制和分享复制继续使用 `CopyFeedbackButton` 管状态,并通过 `actionSurface` 复用动作按钮外观。大鱼吃小鱼结果页资产工坊 footer、关卡主图 / 动作入口和场地背景生成这类白底平台动作也使用 `shape="pill" size="xs"`,深色 hero 返回 / 测试 / 发布按钮保留玩法品牌布局。后续带复制三态的按钮不改用普通 ActionButton避免复制状态分支回流业务页暗色可选项卡继续使用 `PlatformDarkOptionCard`,像素风发送按钮和强品牌动作继续保留专用布局。
19.1. `CopyFeedbackButton` 支持 `actionShape`,用于在复用共享复制状态机时直接对齐 `PlatformActionButton` 的圆角外观;拼图广场详情 hero 的“分享作品”已使用 `actionSurface="editorDark" actionShape="pill"`,不再手写复制按钮 rounded / border / bg class。
19.2. 拼图广场详情 hero 的返回、上一张 / 下一张关卡图入口迁移到 `PlatformIconButton variant="darkMini"`,修改作品和进入第 1 关迁移到 `PlatformActionButton`,分享动作继续使用 `CopyFeedbackButton` 但复用共享动作按钮 chrome详情页只保留轮播、复制和跳转语义不再手写 hero 区按钮壳。
19.3. creative-agent 首页的侧边栏菜单、账号入口、开启新对话、我的创作和首页激励 CTA 迁移到 `PlatformIconButton` / `PlatformActionButton`;首页继续保留 `creative-agent-home__*` 本地 class 承接透明顶栏、抽屉和品牌化胶囊视觉,不把视觉回收和语义收口绑成一次大改。
20. 平台方形上传入口和紧凑虚线新增入口迁移到 `PlatformUploadTile`,上传后的图片预览迁移到 `PlatformUploadPreviewCard`;反馈页上传凭证入口 / 预览、敲木鱼工作台新增功德词条入口、通用创作图片面板的提示词参考图缩略图、抓大鹅封面编辑参考图缩略图、通用输入 Composer 已选参考图条、creation-agent 已选参考图条和拼图结果页关卡引用图横条已先迁移。方形缩略图使用默认 `layout="square"`,横向“已选择参考图 / 文件名 / 素材名 / 移除”条使用 `layout="inline"`;只读引用图条不传 `onRemove`,避免公共组件额外渲染删除入口。后续继续收口结果页素材上传、工作台参考图上传、紧凑虚线新增入口等上传 / 动作块时,业务页只保留文件选择、预览数组、预览回调、删除回调、校验逻辑或新增回调,上传方块外观、主副文案、缩略图壳、预览按钮、标题行、横向已选条、移除按钮和禁用态统一由 Module 承接;工具栏中的小图标上传仍继续使用 `PlatformIconButton asChild="label"`
21. 图片编辑面板中的白底胶囊开关迁移到 `PlatformPillSwitch`;通用创作图片面板和抓大鹅封面编辑的 `AI重绘` 已先迁移。后续同类开关只保留受控布尔值和状态变更回调switch 输入语义、轨道、圆点、白底浮层和禁用态统一由 Module 承接。
22. 设置面板、结果页运行配置和工作台白底配置项中的整行开关迁移到 `PlatformToggleRow`视觉小说结果页、runtime 设置面板和拼消消创作工作台 AI 生成底图开关已先迁移。后续整行配置项只保留字段写回和可选点击动作,不再重复开关行 chrome、checkbox class 或状态 pill。
@@ -223,6 +227,8 @@
- `npm run test -- src/components/common/PlatformReportDialog.test.tsx src/components/platform-entry/PlatformErrorDialog.test.tsx`
- `npm run test -- src/components/common/PlatformReportDialog.test.tsx src/components/platform-entry/PlatformTaskCompletionDialog.test.tsx`
- `npm run test -- src/components/common/CopyFeedbackButton.test.tsx src/components/puzzle-gallery/PuzzleGalleryDetailView.test.tsx`
- `npm run test -- src/components/creative-agent/CreativeAgentHome.test.tsx src/components/auth/BindPhoneScreen.test.tsx`
- `npm run test -- src/components/common/UnifiedConfirmDialog.test.tsx`
- `npm run test -- src/components/common/useCopyFeedback.test.tsx`
- `npm run test -- src/components/common/CopyFeedbackButton.test.tsx`

View File

@@ -47,6 +47,9 @@ test('绑定手机号表单复用平台输入和字段标题', async () => {
expect(screen.getByText('手机号').className).toContain(
'text-[var(--platform-text-strong)]',
);
expect(screen.getByText('当前登录身份:微信旅人').className).toContain(
'platform-subpanel',
);
await user.type(phoneInput, '13800000000');
await user.type(codeInput, '123456');

View File

@@ -5,6 +5,7 @@ import type { AuthCaptchaChallenge, AuthUser } from '../../services/authService'
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import { PlatformTextField } from '../common/PlatformTextField';
import { CaptchaChallengeField } from './CaptchaChallengeField';
@@ -78,9 +79,14 @@ export function BindPhoneScreen({
<p className="mt-4 max-w-md text-sm leading-7 text-[var(--platform-text-base)]">
</p>
<div className="platform-subpanel mt-8 rounded-2xl px-4 py-4 text-sm text-[var(--platform-text-base)]">
<PlatformSubpanel
as="div"
radius="sm"
padding="md"
className="mt-8 text-sm text-[var(--platform-text-base)]"
>
{user.displayName}
</div>
</PlatformSubpanel>
</div>
<form

View File

@@ -103,6 +103,7 @@ test('can opt into platform action button chrome', () => {
state="idle"
idleLabel="复制报错"
actionSurface="platform"
actionShape="pill"
actionFullWidth
/>,
);
@@ -111,6 +112,7 @@ test('can opt into platform action button chrome', () => {
expect(button.className).toContain('platform-button--primary');
expect(button.className).toContain('w-full');
expect(button.className).toContain('rounded-full');
expect(button.className).toContain('disabled:cursor-not-allowed');
});

View File

@@ -4,6 +4,7 @@ import type { ButtonHTMLAttributes, ReactNode } from 'react';
import {
getPlatformActionButtonClassName,
type PlatformActionButtonSize,
type PlatformActionButtonShape,
type PlatformActionButtonSurface,
type PlatformActionButtonTone,
} from './platformActionButtonModel';
@@ -34,6 +35,7 @@ type CopyFeedbackButtonProps = Omit<
actionSurface?: PlatformActionButtonSurface;
actionTone?: PlatformActionButtonTone;
actionSize?: PlatformActionButtonSize;
actionShape?: PlatformActionButtonShape;
actionFullWidth?: boolean;
actionAppearance?: CopyFeedbackButtonActionAppearance;
actionPillTone?: PlatformPillBadgeTone;
@@ -74,6 +76,7 @@ export function CopyFeedbackButton({
actionSurface,
actionTone = 'primary',
actionSize = 'sm',
actionShape = 'default',
actionFullWidth = false,
actionAppearance = 'plain',
actionPillTone = 'neutral',
@@ -112,6 +115,7 @@ export function CopyFeedbackButton({
surface: actionSurface,
tone: actionTone,
size: actionSize,
shape: actionShape,
fullWidth: actionFullWidth,
})
: actionAppearance === 'pill'

View File

@@ -1,6 +1,7 @@
/* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect, test, vi } from 'vitest';
import { CreativeAgentHome } from './CreativeAgentHome';
@@ -45,3 +46,29 @@ test('CreativeAgentHome uses shared status message for home error', () => {
'border-[var(--platform-button-danger-border)]',
);
});
test('CreativeAgentHome reuses shared button chrome for drawer and reward actions', async () => {
const user = userEvent.setup();
renderHome();
const menuButton = screen.getByRole('button', { name: '打开侧边栏' });
const rewardButton = screen.getByRole('button', {
name: '搓闪应用 分1亿激励',
});
expect(menuButton.className).toContain('platform-icon-button');
expect(rewardButton.className).toContain('platform-button');
await user.click(menuButton);
expect(
screen.getByRole('button', { name: '开启新对话' }).className,
).toContain('platform-button');
expect(
screen.getByRole('button', { name: '我的创作' }).className,
).toContain('platform-button');
expect(screen.getByRole('button', { name: '账号' }).className).toContain(
'platform-icon-button',
);
});

View File

@@ -14,6 +14,7 @@ import {
import { useMemo, useState } from 'react';
import type { CreativeAgentInputPart } from '../../../packages/shared/src/contracts/creativeAgent';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import { PlatformIconButton } from '../common/PlatformIconButton';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
@@ -203,30 +204,31 @@ function CreativeAgentDrawer({
</header>
<div className="shrink-0 space-y-3 px-5">
<button
type="button"
<PlatformActionButton
onClick={() => {
onStartNewChat();
onClose();
}}
className="creative-agent-drawer__new-chat"
fullWidth
>
<MessageCircle className="h-5 w-5" />
<span></span>
</button>
</PlatformActionButton>
<button
type="button"
<PlatformActionButton
onClick={() => {
onOpenDrafts();
onClose();
}}
className="creative-agent-drawer__nav-row"
align="start"
fullWidth
>
<Bookmark className="h-5 w-5" />
<span></span>
<ChevronRight className="ml-auto h-4 w-4 opacity-55" />
</button>
</PlatformActionButton>
</div>
<div className="mt-5 min-h-0 flex-1 overflow-y-auto px-5 pb-5">
@@ -261,17 +263,16 @@ function CreativeAgentDrawer({
</div>
<footer className="flex shrink-0 items-center justify-between gap-3 px-5 py-5 pb-[max(1.15rem,env(safe-area-inset-bottom))]">
<button
type="button"
<PlatformIconButton
onClick={() => {
onOpenAccount();
onClose();
}}
className="creative-agent-drawer__avatar"
aria-label="账号"
>
<UserRound className="h-5 w-5" />
</button>
label="账号"
title="账号"
icon={<UserRound className="h-5 w-5" />}
/>
<div className="flex items-center gap-3">
<PlatformIconButton
onClick={() => {
@@ -331,15 +332,13 @@ export function CreativeAgentHome({
<div className="creative-agent-home platform-remap-surface">
<div className="creative-agent-home__backdrop" />
<header className="creative-agent-home__topbar">
<button
type="button"
<PlatformIconButton
onClick={() => setDrawerOpen(true)}
className="creative-agent-home__topbar-button"
aria-label="打开侧边栏"
label="打开侧边栏"
title="菜单"
>
<Menu className="h-6 w-6" />
</button>
icon={<Menu className="h-6 w-6" />}
/>
<RpgEntryBrandLogo className="creative-agent-home__brand" decorative />
</header>
@@ -358,15 +357,15 @@ export function CreativeAgentHome({
onClick={() => submitText(item.prompt)}
/>
))}
<button
type="button"
<PlatformActionButton
className="creative-agent-home__reward"
disabled={isBusy}
onClick={() => submitText('帮我做一个能马上分享的创意拼图。')}
shape="pill"
>
<Sparkles className="h-6 w-6" />
<span> 1亿</span>
</button>
</PlatformActionButton>
</div>
{error ? (

View File

@@ -180,6 +180,61 @@ test('reuses platform subpanel and pill badge chrome for puzzle detail metadata'
);
});
test('reuses shared action button chrome for puzzle detail hero actions', () => {
render(
<PuzzleGalleryDetailView
item={{
...detailItem,
coverImageSrc: '/hero-cover.png',
levels: [
{
levelId: 'level-1',
levelName: '第一关',
pictureDescription: '第一关画面',
selectedCandidateId: null,
coverImageSrc: '/level-1.png',
coverAssetId: null,
generationStatus: 'ready',
candidates: [],
},
{
levelId: 'level-2',
levelName: '第二关',
pictureDescription: '第二关画面',
selectedCandidateId: null,
coverImageSrc: '/level-2.png',
coverAssetId: null,
generationStatus: 'ready',
candidates: [],
},
],
}}
onBack={vi.fn()}
onEdit={vi.fn()}
onStartGame={vi.fn()}
/>,
);
expect(screen.getByRole('button', { name: '修改作品' }).className).toContain(
'platform-action-button--editor-dark',
);
expect(screen.getByRole('button', { name: '分享作品' }).className).toContain(
'platform-action-button--editor-dark',
);
expect(
screen.getByRole('button', { name: '进入第 1 关' }).className,
).toContain('platform-action-button--accent');
expect(screen.getByRole('button', { name: '返回' }).className).toContain(
'rounded-full',
);
expect(
screen.getByRole('button', { name: '上一张关卡图' }).className,
).toContain('rounded-full');
expect(
screen.getByRole('button', { name: '下一张关卡图' }).className,
).toContain('rounded-full');
});
test('falls back to legacy selection copy when clipboard api rejects', async () => {
const user = userEvent.setup();
const writeText = vi.fn(async () => {

View File

@@ -14,6 +14,8 @@ import { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
import { buildPuzzlePublicWorkCode } from '../../services/publicWorkCode';
import { CopyCodeButton } from '../common/CopyCodeButton';
import { CopyFeedbackButton } from '../common/CopyFeedbackButton';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformIconButton } from '../common/PlatformIconButton';
import { PlatformMediaFrame } from '../common/PlatformMediaFrame';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
@@ -136,25 +138,27 @@ export function PuzzleGalleryDetailView({
<div className="mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col gap-3 overflow-hidden px-1 sm:px-0">
<div className="relative overflow-hidden rounded-[1.8rem] border border-amber-100/16 bg-[radial-gradient(circle_at_top_left,rgba(251,191,36,0.18),transparent_32%),linear-gradient(135deg,rgba(76,29,19,0.98),rgba(15,23,42,0.98))] px-4 py-4 text-white sm:px-5">
<div className="flex items-start justify-between gap-3">
<button
type="button"
<PlatformIconButton
onClick={onBack}
aria-label="返回"
className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-white/16 bg-white/10 text-white/84"
>
<ArrowLeft className="h-4 w-4" />
</button>
label="返回"
title="返回"
variant="darkMini"
className="h-10 w-10 !border-white/16 !bg-white/10 !text-white/84 backdrop-blur hover:!bg-white/16 hover:!text-white"
icon={<ArrowLeft className="h-4 w-4" />}
/>
<div className="flex flex-wrap justify-end gap-2">
{onEdit ? (
<button
type="button"
<PlatformActionButton
disabled={isBusy}
onClick={onEdit}
className="inline-flex items-center gap-2 rounded-full bg-white/12 px-4 py-2 text-sm font-bold text-white disabled:opacity-45"
surface="editorDark"
tone="secondary"
shape="pill"
className="!border-white/16 !bg-white/12 !text-white hover:!bg-white/18"
>
<Pencil className="h-4 w-4" />
</button>
</PlatformActionButton>
) : null}
<CopyFeedbackButton
state={shareState}
@@ -162,17 +166,20 @@ export function PuzzleGalleryDetailView({
onClick={sharePublicWork}
idleLabel="分享作品"
idleIcon={<Share2 className="h-4 w-4" />}
className="inline-flex items-center gap-2 rounded-full bg-white/12 px-4 py-2 text-sm font-bold text-white disabled:opacity-45"
actionSurface="editorDark"
actionTone="secondary"
actionShape="pill"
className="!border-white/16 !bg-white/12 !text-white hover:!bg-white/18"
/>
<button
type="button"
<PlatformActionButton
disabled={isBusy}
onClick={onStartGame}
className="inline-flex items-center gap-2 rounded-full bg-amber-200 px-4 py-2 text-sm font-bold text-slate-950 disabled:opacity-45"
tone="accent"
shape="pill"
>
<Play className="h-4 w-4" />
1
</button>
</PlatformActionButton>
</div>
</div>
@@ -231,24 +238,22 @@ export function PuzzleGalleryDetailView({
>
{coverImageSrc && hasCoverCarousel ? (
<>
<button
type="button"
<PlatformIconButton
onClick={showPreviousCover}
className="absolute left-3 top-1/2 z-10 inline-flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-white/30 bg-slate-950/36 text-white backdrop-blur"
aria-label="上一张关卡图"
label="上一张关卡图"
title="上一张关卡图"
>
<ChevronLeft className="h-5 w-5" />
</button>
<button
type="button"
variant="darkMini"
className="absolute left-3 top-1/2 z-10 h-10 w-10 -translate-y-1/2 !border-white/30 !bg-slate-950/36 !text-white backdrop-blur hover:!bg-slate-950/52"
icon={<ChevronLeft className="h-5 w-5" />}
/>
<PlatformIconButton
onClick={showNextCover}
className="absolute right-3 top-1/2 z-10 inline-flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-white/30 bg-slate-950/36 text-white backdrop-blur"
aria-label="下一张关卡图"
label="下一张关卡图"
title="下一张关卡图"
>
<ChevronRight className="h-5 w-5" />
</button>
variant="darkMini"
className="absolute right-3 top-1/2 z-10 h-10 w-10 -translate-y-1/2 !border-white/30 !bg-slate-950/36 !text-white backdrop-blur hover:!bg-slate-950/52"
icon={<ChevronRight className="h-5 w-5" />}
/>
<div className="absolute inset-x-4 bottom-3 z-10 flex justify-center gap-1.5">
{coverSlides.map((slide, index) => (
<button