Skip to content

Commit b59e33a

Browse files
authored
#602 [PR 9] Web UI 完成初步统一并进行双主题升级, MVP 版本 (Global UI/UX Optimization) (#869)
* feat(web): extract phase-0 design tokens and stabilize shell baseline Background: - PR9 needed a stable styling foundation before page-level polish could be grouped coherently. - The web shell still mixed early layout DNA with page-local treatments, which made later visual cleanup noisy. What changed: - Extracted the initial phase-0 style tokens into the shared stylesheet baseline. - Tightened shell and sidebar-adjacent framing so shared layout surfaces have a cleaner default edge treatment. - Aligned common header and toolbar primitives with the phase-0 styling baseline. - Added the cn utility test coverage that belongs with the shared styling foundation. Why this grouping: - These changes form the lowest-level visual baseline for the rest of the PR9 cleanup work. - Keeping them together makes later page-focused commits easier to review and to revert independently. Validation: - Not run in this history-cleanup step; final branch verification will cover lint and build checks. Risk: - Shared shell and global stylesheet adjustments can affect multiple pages at once if any token mapping is off. * refactor(web): restore settings theme consistency and light-mode hierarchy Background: - The settings area had drifted away from the restored theme system, especially in light mode where hierarchy, borders, and field framing no longer matched the password and auth surfaces. - PR9 needed a dedicated settings pass so later page cleanup would not keep reintroducing local fixes. What changed: - Re-aligned settings cards, category navigation, field rows, and loading states with the shared theme baseline. - Restored theme-toggle behavior and tightened the visual contract around settings-specific inputs and editors. - Normalized related shared controls used by settings forms so select and badge treatments match the refreshed theme hierarchy. - Updated the main stylesheet to give settings light-mode surfaces clearer spacing, contrast, and resting borders. Why this grouping: - These changes are all part of one coherent settings-theme recovery pass. - Keeping settings recovery separate from chat, home, and backtest polish makes the later review path much easier to reason about. Validation: - Not run in this history-cleanup step; final branch verification will cover lint and build checks. Risk: - Theme hierarchy adjustments in shared form controls may subtly affect any page that reuses the same select or badge defaults. * feat(web): add shared status primitives and refine chat-home interaction surfaces Background: - PR9 needed reusable status and tooltip primitives before the remaining page-level cleanup could converge on one visual language. - Chat and home still carried a mix of page-local interaction cues, making report surfaces and message flow harder to keep consistent. What changed: - Added shared Tooltip and StatusDot primitives and wired them into the common component exports. - Hardened tooltip rendering around report surfaces so overlays no longer clip inside constrained containers. - Refined chat message flow, action affordances, session controls, and progress cues to support the non-blocking interaction model. - Tightened home and report-facing surfaces, including shared card and input treatments that support the updated report hierarchy. - Updated the related chat and home tests that move with these interaction changes. Why this grouping: - These changes all depend on the same shared primitive layer and mainly affect chat, home, and report-facing interaction surfaces. - Keeping them in one commit isolates the reusable UI foundation from the later state-unification and final regression passes. Validation: - Not run in this history-cleanup step; final branch verification will cover lint and build checks. Risk: - Shared tooltip behavior, report surface styling, and chat interaction flow are cross-cutting and can regress multiple views if any container assumptions changed. * refactor(web): unify empty states, status feedback, portfolio-backtest surfaces, and coverage Background: - After the shared primitive pass, PR9 still had fragmented state presentation across home, portfolio, backtest, and adjacent navigation flows. - Empty states, task/status feedback, and portfolio/backtest surfaces needed to converge before the final regression cleanup could be reviewed sanely. What changed: - Unified empty-state and status-feedback treatments across home, task, portfolio, and backtest flows. - Brought portfolio and backtest surfaces closer to the shared input-surface and table styling conventions, including the remaining page-level polish for their primary layouts. - Tightened supporting shared components and sidebar behavior that participate in the refreshed state presentation. - Added and expanded smoke, sidebar, backtest, and portfolio-related test coverage that belongs with this behavior shift. - Updated the global stylesheet to carry the state, alert, and table refinements that power these pages. Why this grouping: - These changes are one coherent pass over the state-feedback layer and the portfolio/backtest page family. - Keeping them together isolates the broad PR9 convergence work from the final light-theme regression fixes that follow. Validation: - Not run in this history-cleanup step; final branch verification will cover lint and build checks. Risk: - This commit touches multiple status-bearing pages and shared stylesheet rules, so regressions would most likely show up in empty states, task badges, table surfaces, or auth smoke flow assumptions. * fix(web): close final light-theme regressions in login and chat presentation Background: - After the broader PR9 convergence work, a final pass was still needed to remove light-theme readability regressions and presentation rough edges in login and chat-adjacent surfaces. - These were the last visible issues before the branch could be treated as a coherent PR9 cleanup line. What changed: - Restored light-theme contrast and text hierarchy in the affected login, chat history, and supporting content surfaces. - Refined chat heading presentation and the sentiment gauge glow so the final visual hierarchy is clearer without adding new interaction patterns. - Added the lightweight UI-governance coverage that belongs with these final presentation constraints. - Applied the small follow-up stylesheet and tooltip adjustments required to support the final polish. Why this grouping: - These changes are intentionally limited to the last regression-fix pass rather than another broad refactor. - Keeping them in one final commit makes the cleanup history easier to review and gives a clear rollback point for the last-mile polish. Validation: - Not run in this history-cleanup step; final branch verification will cover lint and build checks. Risk: - The remaining risk is concentrated in light-theme readability and chat-adjacent presentation details, especially where shared text and gauge styling overlap. * fix(web): restore remaining PR9 state-feedback parity after regrouping Background: - The history regrouping left a small set of state-feedback and page-surface files out of sync with the original PR9 branch. - Those differences were accidental cleanup omissions rather than intended design changes, so they need an explicit parity restore commit. What changed: - Restored the remaining Home, Portfolio, Settings, and task/status presentation updates to match the original PR9 branch content. - Brought the supporting stylesheet and home-page test expectation back to parity with the source branch. - Reinstated the intended SettingsAlert and page-level state-feedback treatments that belonged in the PR9 convergence work. Why this grouping: - These changes are a narrow parity-recovery pass caused by the history rewrite itself. - Keeping them together makes it obvious that the commit exists to eliminate cleanup drift rather than introduce new behavior. Validation: - Not run in this history-cleanup step; final branch verification will cover lint and build checks. Risk: - The remaining risk is limited to state-feedback presentation on Home, Portfolio, and Settings, because this commit only restores source-branch parity for those areas. * fix(web): keep chat message actions reachable on touch devices * update docs * fix(web): restore login page light theme * fix(web): restore login theme tokens
1 parent 1d78793 commit b59e33a

55 files changed

Lines changed: 2497 additions & 812 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
| 基本面 | 结构化聚合 | 新增 `fundamental_context`(valuation/growth/earnings/institution/capital_flow/dragon_tiger/boards,其中 `earnings.data` 新增 `financial_report``dividend``boards` 表示板块涨跌榜),主链路 fail-open 降级 |
4242
| 策略 | 市场策略系统 | 内置 A股「三段式复盘策略」与美股「Regime Strategy」,输出进攻/均衡/防守或 risk-on/neutral/risk-off 计划,并附“仅供参考,不构成投资建议”提示 |
4343
| 复盘 | 大盘复盘 | 每日市场概览、板块涨跌;支持 cn(A股)/us(美股)/both(两者) 切换 |
44+
| 界面 | 双主题工作台 | Web 工作台现支持全新浅色主题与深色主题切换,首页 / 问股 / 回测 / 持仓 / 设置统一升级为同一套视觉与交互体系 |
4445
| 补全 | 智能补全 (MVP) | **[测试阶段]** 首页搜索框支持代码/名称/拼音/别名联想;索引已覆盖 A股、港股、美股三个市场,支持通过 Tushare 或 AkShare 数据源更新 |
4546
| 智能导入 | 多源导入 | 支持图片、CSV/Excel 文件、剪贴板粘贴;Vision LLM 提取代码+名称;置信度分层确认;名称→代码解析(本地+拼音+AkShare) |
4647
| 历史记录 | 批量管理 | 支持多选、全选及批量删除历史分析记录,优化管理效率与 UI/UX 体验 |
@@ -51,7 +52,9 @@
5152

5253
> 历史报告详情会优先展示 AI 返回的原始「狙击点位」文本,避免区间价、条件说明等复杂内容在历史回看时被压缩成单个数字。
5354
54-
> Web 界面做小调整:首页进一步收敛了样式层级与移动端滚动体验,修复bug;问股页为 AI 回复新增消息复制按钮,并补强了会话导出、通知发送、历史删除和追问参数保护;首页「原始分析结果 / 分析快照」复制反馈改为按面板独立。
55+
> Web 工作台已完成一轮 UI 升级:新增完整浅色主题,并支持浅色 / 深色主题一键切换;首页、问股、回测、持仓、设置页改为共享同一套设计 token、输入表面、状态反馈和抽屉/滚动语义,日常使用与移动端浏览更连贯。
56+
57+
> 问股页进一步补强了消息复制、会话导出、通知发送、历史删除和追问上下文保护;首页与报告抽屉的复制反馈、空态和错误态也已按面板独立收口,减少误触和状态串扰。
5558
5659
> Web 管理认证支持运行时开关;如果系统中已保留管理员密码,重新开启认证时必须提供当前密码,避免在认证关闭窗口内直接获取新的管理员会话。
5760
> 多进程/多 worker 部署时,认证开关仅在当前进程即时生效;需重启或滚动重启全部 worker 以统一状态。
@@ -333,6 +336,13 @@ LITELLM_MODEL=openai/deepseek-chat
333336

334337
包含完整的配置管理、任务监控和手动分析功能。
335338

339+
**本轮界面升级亮点:**
340+
341+
- **全新浅色主题**:不再只是深色工作台,浅色模式针对输入框、卡片层级、边界对比、状态提示做了成套重绘。
342+
- **主题切换可持久化**:侧边栏内可随时切换浅色 / 深色主题,页面刷新后仍保留用户选择。
343+
- **核心页面统一收口**:首页、问股、回测、持仓、设置共用同一套视觉 token、状态原语和表面组件,减少“每页一个风格”的割裂感。
344+
- **移动端与触屏体验增强**:抽屉遮罩、滚动契约、消息操作可达性和关键按钮语义同步补强,小屏设备上更稳定。
345+
336346
**可选密码保护**:在 `.env` 中设置 `ADMIN_AUTH_ENABLED=true` 可启用 Web 登录,首次访问在网页设置初始密码,保护 Settings 中的 API 密钥等敏感配置。系统设置现支持运行时开启或关闭认证;关闭认证不会删除已保存密码,后续可直接重新启用。认证开启时,`POST /api/v1/auth/logout` 也需要有效会话;如果会话已经过期,前端会直接回到登录页。详见 [完整指南](docs/full-guide.md)
337347

338348
### 智能导入
@@ -386,7 +396,7 @@ LITELLM_MODEL=openai/deepseek-chat
386396
- **自然语言提问**:如「用缠论分析 600519」,Agent 自动调用实时行情、K线、技术指标、新闻等工具
387397
- **流式进度反馈**:实时展示 AI 思考路径(行情获取 → 技术分析 → 新闻搜索 → 生成结论)
388398
- **多轮对话**:支持追问上下文,会话历史持久化保存
389-
- **导出与发送**:可将会话导出为 .md 文件,或发送到已配置的通知渠道
399+
- **导出与发送**:可将会话导出为 `.md` 文件,或发送到已配置的通知渠道,并提供明确的成功/失败反馈
390400
- **后台执行**:切换页面不中断分析,完成时 Dock 问股图标显示角标
391401
- **Bot 命令**`/ask` 技能分析(支持多股对比)、`/chat` 自由对话
392402
- **自定义策略(Skill)**:在 `strategies/` 目录下新建 YAML 文件或在自定义 skill 目录中放入 `SKILL.md` bundle,即可添加新的交易策略,无需写代码

apps/dsa-web/e2e/smoke.spec.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,24 @@ const smokePassword = process.env.DSA_WEB_SMOKE_PASSWORD;
55
async function login(page: Page) {
66
test.skip(!smokePassword, 'Set DSA_WEB_SMOKE_PASSWORD to run authenticated smoke tests.');
77

8-
// Navigate to login page
98
await page.goto('/login');
109
await page.waitForLoadState('domcontentloaded');
1110

12-
// Wait for password input to be visible
13-
await expect(page.locator('#password')).toBeVisible({ timeout: 10_000 });
11+
const passwordInput = page.locator('#password');
12+
const submitButton = page.getByRole('button', { name: /|/ });
13+
const homeLink = page.getByRole('link', { name: '首页' });
1414

15-
// Fill password and submit
16-
await page.locator('#password').fill(smokePassword!);
15+
const isAlreadyAuthenticated =
16+
page.url().endsWith('/') ||
17+
await homeLink.isVisible({ timeout: 2_000 }).catch(() => false);
1718

18-
// Wait for and click the submit button
19-
const submitButton = page.getByRole('button', { name: /|/ });
19+
if (isAlreadyAuthenticated) {
20+
await page.waitForLoadState('domcontentloaded');
21+
return;
22+
}
23+
24+
await expect(passwordInput).toBeVisible({ timeout: 10_000 });
25+
await passwordInput.fill(smokePassword!);
2026
await expect(submitButton).toBeVisible();
2127

2228
await Promise.all([
@@ -27,7 +33,6 @@ async function login(page: Page) {
2733
submitButton.click(),
2834
]);
2935

30-
// Wait for navigation to home page after login
3136
await page.waitForURL('/', { timeout: 15_000 });
3237
await page.waitForLoadState('domcontentloaded');
3338
await page.waitForTimeout(1000);
@@ -86,6 +91,23 @@ test.describe('web smoke', () => {
8691
await expect(page.locator('p').filter({ hasText: prompt }).last()).toBeVisible({ timeout: 5000 });
8792
});
8893

94+
test('chat page uses accessible labels instead of native title attributes for key actions', async ({ page }) => {
95+
await login(page);
96+
97+
await page.getByRole('link', { name: '问股' }).click();
98+
await page.waitForLoadState('domcontentloaded');
99+
100+
const sendButton = page.getByRole('button', { name: '发送' });
101+
const composer = page.getByPlaceholder(/ 600519/);
102+
103+
await expect(page.getByTestId('chat-workspace')).toBeVisible({ timeout: 10_000 });
104+
await expect(sendButton).toBeVisible({ timeout: 10_000 });
105+
await expect(composer).toBeVisible({ timeout: 10_000 });
106+
107+
await expect(sendButton).not.toHaveAttribute('title', /.+/);
108+
await expect(composer).not.toHaveAttribute('title', /.+/);
109+
});
110+
89111
test('mobile shell opens navigation drawer after login', async ({ page }) => {
90112
await page.setViewportSize({ width: 390, height: 844 });
91113
await login(page);

apps/dsa-web/eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint'
66
import { defineConfig, globalIgnores } from 'eslint/config'
77

88
export default defineConfig([
9-
globalIgnores(['dist']),
9+
globalIgnores(['dist', 'playwright-report', 'test-results']),
1010
{
1111
files: ['**/*.{ts,tsx}'],
1212
extends: [

apps/dsa-web/src/components/StockAutocomplete/StockAutocomplete.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import { useAutocomplete } from '../../hooks/useAutocomplete';
1414
import { SuggestionsList } from './SuggestionsList';
1515
import { cn } from '../../utils/cn';
1616

17+
const AUTOCOMPLETE_INPUT_CLASS =
18+
'input-surface input-focus-glow h-11 w-full rounded-xl border bg-transparent px-4 text-sm transition-all focus:outline-none disabled:cursor-not-allowed disabled:opacity-60';
19+
1720
export interface StockAutocompleteProps {
1821
/** Input value */
1922
value: string;
@@ -49,7 +52,7 @@ function FallbackInput({
4952
}}
5053
placeholder={placeholder}
5154
disabled={disabled}
52-
className={cn('input-terminal w-full', className)}
55+
className={cn(AUTOCOMPLETE_INPUT_CLASS, className)}
5356
data-autocomplete-mode="fallback"
5457
/>
5558
);
@@ -251,8 +254,7 @@ function StockAutocompleteInner({
251254
placeholder={placeholder}
252255
disabled={disabled}
253256
className={cn(
254-
"input-terminal w-full",
255-
"focus:outline-none",
257+
AUTOCOMPLETE_INPUT_CLASS,
256258
isOpen && "rounded-b-none",
257259
className
258260
)}

apps/dsa-web/src/components/StockAutocomplete/SuggestionsList.tsx

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import type { CSSProperties } from 'react';
99
import type { StockSuggestion } from '../../types/stockIndex';
10+
import { Badge } from '../common';
1011
import { cn } from '../../utils/cn';
1112

1213
export interface SuggestionsListProps {
@@ -83,12 +84,12 @@ export function SuggestionsList({
8384

8485
// Helper component: Market badge
8586
const MARKET_BADGE_CONFIG = {
86-
CN: { label: 'A股', className: 'text-red-500 bg-red-500/10' },
87-
HK: { label: '港股', className: 'text-green-500 bg-green-500/10' },
88-
US: { label: '美股', className: 'text-blue-500 bg-blue-500/10' },
89-
INDEX: { label: '指数', className: 'text-purple-500 bg-purple-500/10' },
90-
ETF: { label: 'ETF', className: 'text-yellow-500 bg-yellow-500/10' },
91-
BSE: { label: '北交所', className: 'text-orange-500 bg-orange-500/10' },
87+
CN: { label: 'A股', className: 'border-danger/25 bg-danger/10 text-danger' },
88+
HK: { label: '港股', className: 'border-success/25 bg-success/10 text-success' },
89+
US: { label: '美股', className: 'border-cyan/25 bg-cyan/10 text-cyan' },
90+
INDEX: { label: '指数', className: 'border-purple/25 bg-purple/10 text-purple' },
91+
ETF: { label: 'ETF', className: 'border-warning/25 bg-warning/10 text-warning' },
92+
BSE: { label: '北交所', className: 'border-orange-500/25 bg-orange-500/10 text-orange-500' },
9293
} as const;
9394

9495
function MarketBadge({ market }: { market: string }) {
@@ -99,27 +100,27 @@ function MarketBadge({ market }: { market: string }) {
99100
}
100101

101102
return (
102-
<span className={cn("text-xs px-2 py-0.5 rounded", config.className)}>
103+
<Badge variant="default" size="sm" className={cn("min-w-[3rem] justify-center shadow-none", config.className)}>
103104
{config.label}
104-
</span>
105+
</Badge>
105106
);
106107
}
107108

108109
// Helper component: Match type badge
109110
function MatchTypeBadge({ matchType }: { matchType: string }) {
110111
const configMap = {
111-
exact: { label: '精确', className: 'bg-cyan/10 text-cyan' },
112-
prefix: { label: '前缀', className: 'bg-purple/10 text-purple' },
113-
contains: { label: '包含', className: 'bg-yellow/10 text-yellow' },
114-
fuzzy: { label: '模糊', className: 'bg-gray/10 text-gray' },
112+
exact: { label: '精确', className: 'border-cyan/25 bg-cyan/10 text-cyan' },
113+
prefix: { label: '前缀', className: 'border-purple/25 bg-purple/10 text-purple' },
114+
contains: { label: '包含', className: 'border-warning/25 bg-warning/10 text-warning' },
115+
fuzzy: { label: '模糊', className: 'border-border/55 bg-elevated/75 text-muted-text' },
115116
};
116117

117118
const config = configMap[matchType as keyof typeof configMap] || configMap.fuzzy;
118119

119120
return (
120-
<span className={cn("text-xs px-1.5 py-0.5 rounded", config.className)}>
121+
<Badge variant="default" size="sm" className={cn("shrink-0 shadow-none", config.className)}>
121122
{config.label}
122-
</span>
123+
</Badge>
123124
);
124125
}
125126

apps/dsa-web/src/components/common/Badge.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@ import { cn } from '../../utils/cn';
33

44
type BadgeVariant = 'default' | 'success' | 'warning' | 'danger' | 'info' | 'history';
55

6-
interface BadgeProps {
6+
interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
77
children: React.ReactNode;
88
variant?: BadgeVariant;
99
size?: 'sm' | 'md';
1010
glow?: boolean;
1111
className?: string;
12+
style?: React.CSSProperties;
1213
}
1314

1415
const variantStyles: Record<BadgeVariant, string> = {
15-
default: 'border-border/40 bg-elevated/40 text-secondary-text',
16+
default: 'border-border/55 bg-elevated/75 text-secondary-text',
1617
success: 'border-success/20 bg-success/10 text-success',
1718
warning: 'border-warning/20 bg-warning/10 text-warning',
1819
danger: 'border-danger/20 bg-danger/10 text-danger',
19-
info: 'border-cyan/20 bg-cyan/10 text-cyan',
20+
info: 'border-cyan/30 bg-cyan/12 text-cyan',
2021
history: 'border-purple/20 bg-purple/10 text-purple',
2122
};
2223

@@ -38,11 +39,15 @@ export const Badge: React.FC<BadgeProps> = ({
3839
size = 'sm',
3940
glow = false,
4041
className = '',
42+
style,
43+
...rest
4144
}) => {
4245
const sizeStyles = size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-3 py-1 text-sm';
4346

4447
return (
4548
<span
49+
{...rest}
50+
style={style}
4651
className={cn(
4752
'inline-flex items-center gap-1 rounded-full border font-medium backdrop-blur-sm',
4853
sizeStyles,

apps/dsa-web/src/components/common/Button.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import { cn } from '../../utils/cn';
33

44
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
55
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'gradient' | 'danger' | 'danger-subtle' | 'settings-primary' | 'settings-secondary' | 'action-primary' | 'action-secondary' | 'home-action-ai' | 'home-action-report';
6-
size?: 'sm' | 'md' | 'lg' | 'xl';
6+
size?: 'xsm' | 'sm' | 'md' | 'lg' | 'xl';
77
isLoading?: boolean;
88
/** Custom loading text. */
99
loadingText?: string;
1010
glow?: boolean;
1111
}
1212

1313
const BUTTON_SIZE_STYLES = {
14+
xsm: 'h-6 rounded-lg px-2 text-sm',
1415
sm: 'h-9 rounded-lg px-3 text-sm',
1516
md: 'h-10 rounded-xl px-4 text-sm',
1617
lg: 'h-11 rounded-xl px-5 text-sm',

apps/dsa-web/src/components/common/Card.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ interface CardProps {
66
subtitle?: string;
77
children: React.ReactNode;
88
className?: string;
9+
style?: React.CSSProperties;
910
variant?: 'default' | 'bordered' | 'gradient';
1011
hoverable?: boolean;
1112
padding?: 'none' | 'sm' | 'md' | 'lg';
@@ -19,6 +20,7 @@ export const Card: React.FC<CardProps> = ({
1920
subtitle,
2021
children,
2122
className = '',
23+
style,
2224
variant = 'default',
2325
hoverable = false,
2426
padding = 'md',
@@ -40,7 +42,7 @@ export const Card: React.FC<CardProps> = ({
4042

4143
if (variant === 'gradient') {
4244
return (
43-
<div className={cn(variantStyles.gradient, className)}>
45+
<div className={cn(variantStyles.gradient, className)} style={style}>
4446
<div className={cn('gradient-border-card-inner', paddingStyles[padding])}>
4547
{(title || subtitle) && (
4648
<div className="mb-3">
@@ -56,6 +58,7 @@ export const Card: React.FC<CardProps> = ({
5658

5759
return (
5860
<div
61+
style={style}
5962
className={cn('rounded-2xl', variantStyles[variant], hoverStyles, paddingStyles[padding], className)}
6063
>
6164
{(title || subtitle) && (

apps/dsa-web/src/components/common/Drawer.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface DrawerProps {
1212
width?: string;
1313
zIndex?: number;
1414
side?: 'left' | 'right';
15+
backdropClassName?: string;
1516
}
1617

1718
/**
@@ -25,6 +26,7 @@ export const Drawer: React.FC<DrawerProps> = ({
2526
width = 'max-w-2xl',
2627
zIndex = 50,
2728
side = 'right',
29+
backdropClassName,
2830
}) => {
2931
// Close the drawer when Escape is pressed.
3032
const handleKeyDown = useCallback(
@@ -64,7 +66,10 @@ export const Drawer: React.FC<DrawerProps> = ({
6466
<div className="fixed inset-0 overflow-hidden" style={{ zIndex }} role="presentation">
6567
{/* Backdrop */}
6668
<div
67-
className="absolute inset-0 bg-background/80 backdrop-blur-sm transition-opacity duration-300"
69+
className={cn(
70+
'absolute inset-0 bg-background/80 backdrop-blur-sm transition-opacity duration-300',
71+
backdropClassName,
72+
)}
6873
onClick={onClose}
6974
/>
7075

apps/dsa-web/src/components/common/InlineAlert.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ export const InlineAlert: React.FC<InlineAlertProps> = ({
2626
className = '',
2727
}) => {
2828
return (
29-
<div className={cn('rounded-2xl border px-4 py-3 shadow-soft-card', variantStyles[variant], className)}>
29+
<div
30+
role="alert"
31+
className={cn('rounded-2xl border px-4 py-3 shadow-soft-card', variantStyles[variant], className)}
32+
>
3033
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
3134
<div>
3235
{title ? <p className="text-sm font-semibold">{title}</p> : null}

0 commit comments

Comments
 (0)