Commit 3334a511 authored by Andy-bubu's avatar Andy-bubu

feat: align xinyuan shell and brand chart theme

parent d3165549
......@@ -7,7 +7,7 @@ Umi route /ai_chat/
-> root theme and CSS imports
-> HeroUI Pro AppShell
-> Sidebar: identity, new session, conversations, app switcher, user menu
-> Navbar: Sidebar.Trigger, route title, page actions, theme action
-> Navbar: Sidebar.Trigger, route title, page actions, notification action
-> page canvas: AI workspace
-> conversation/content surface
-> fixed PromptInput composer
......@@ -24,7 +24,7 @@ Umi route /ai_chat/
- 页面/controller 保留业务 state、API 参数、回调、格式转换、请求时机和权限判断。
- HeroUI/HeroUI Pro 组件拥有可见控件、ARIA、焦点、键盘交互、Portal 和视觉状态。
- 标准弹窗通过共享 `rc-dialog-panel--{size}` 尺寸梯度约束宽度;内容型配置/导入弹窗使用 `4xl = 1088px`,桌面保留 40px 安全边距,手机保留 16px 安全边距。
- 页面级短命令通过 AppShell 的 Navbar actions 插槽渲染在主题按钮左侧;窄屏使用 HeroUI Dropdown,页面正文不重复同一状态摘要。
- 页面级短命令通过 AppShell 的 Navbar actions 插槽渲染在通知按钮左侧;窄屏使用 HeroUI Dropdown,页面正文不重复同一状态摘要。
- AI 工作区由固定视口高度承载;`ChatConversation` 是唯一纵向滚动区域,PromptInput 作为 flex 尾部固定在底部。PromptInput 使用 HeroUI Pro 官方 `stacked`/`lg` 组合,保持附件、停止生成、发送和自动扩展行为。
- 共享组件只能是业务无关的 HeroUI-native composition,不得复刻 Ant/旧组件 props、事件或 DOM。
- Ant Design Icons 可作为纯图标目录保留;不得通过它包装 UI primitive。
......
......@@ -17,7 +17,7 @@
- [x] 引入权威 `theme.css` 原始副本
- [x] 引入批准的 `tokens.css` 与 Shell 布局 Tokens
- [x] 建立 Tailwind -> HeroUI -> Pro -> theme -> tokens 顺序
- [x] 根级浅/深色与 Portal 主题继承
- [x] 固定浅色内容区、深色 Sidebar 与 Portal 主题继承
- [x] 活动页面 Ant Design/Ant Design X/antd-style 视觉引用归零
- [x] 生产解析固定为 React 19;浏览器无重复 Context/slot/provider 警告
- [x] Inter 从 Umi 热更新 CSS 移出,改由稳定 HTML link 加载本地 400/500/600 WOFF2
......@@ -28,10 +28,10 @@
- [x] 账号、密码、验证码 40px 等高与 filled/autofill 状态
- [x] HeroUI Pro Sidebar 240px 与真 offcanvas
- [x] HeroUI Pro Navbar、Sidebar.Trigger 与 48/32/16px 对称边距
- [x] 主题切换及持久化
- [x] 删除主题切换入口,固定浅色内容区
- [x] Sidebar 用户 Dropdown 与真实退出登录
- [x] 应用切换 HeroUI Modal、两列应用 Card、36px identity well,以及 `site.group` 动态分组、真实数量和“全部应用/其他应用”回退
- [x] 移动端 Sidebar.Mobile
- [x] 移动端 Pro Sheet Sidebar
## AI 与业务模块
......@@ -39,7 +39,7 @@
- [x] 主对话列表、PromptInput、附件和发送/停止状态
- [x] 消息区独立滚动、PromptInput 底部固定,并提升为 HeroUI Pro stacked/lg 高度布局
- [x] 空会话使用 HeroUI Pro PromptSuggestion 原生分组与 Card 网格;单项满行、双项两列、移动单列,PromptInput 使用 compact 布局
- [x] 审计知识库操作迁入 Navbar 主题按钮左侧,移动端使用 HeroUI Dropdown;删除 PromptInput 上方重复的文件与数量摘要
- [x] 审计知识库操作迁入 Navbar 通知按钮左侧,移动端使用 HeroUI Dropdown;删除 PromptInput 上方重复的文件与数量摘要
- [~] 审计知识库状态、配置、导入、结果与导出:DropZone/Modal 已验;无授权 Excel,DataGrid 真实行待复验
- [x] 发票助手上传、步骤、确认和下载的组件边界迁移
- [x] 所有 Modal/AlertDialog/Dropdown/Tooltip/DropZone 使用 HeroUI 根
......@@ -83,7 +83,7 @@
- [x] 确认应用为“仅消息通知”范围;功能 1、2、3、5 不适用且未实施
- [x] 审计 `current_user`、`applications[]`、basePath、Shell 数据流、通知接口和现有回调
- [x] 新增独立 `/notifications` 路由,保留 `/ai_chat/` base
- [x] Navbar 使用 HeroUI Badge.Anchor、Badge、Button、Tooltip;36px Bell 位于主题按钮左侧
- [x] Navbar 使用 HeroUI Badge.Anchor、Badge、Button、Tooltip;36px Bell 位于右侧
- [x] Sidebar 无通知入口,未读 0/1/99/100 显示规则通过
- [x] 消息/待办、全部/未读、搜索、防抖、分页、单条/全部已读通过
- [x] 动态 action 路径及 `{notice_id}` 请求体通过
......@@ -93,3 +93,11 @@
- [x] 与“问题整改”同视口对照通过;目标通知页控制台 0 error / 0 warning
- [x] 浏览器内存夹具未进入源码、文档或业务数据
- [x] Andy 真实验证码登录、空通知接口、Sidebar 真实退出通过
## 图表配色专项
- [x] 品牌主色固定为 `#DA241A`,primary/accent/focus/hover/pressed 色阶统一
- [x] 建立 `xinyuan-chart-1..16` 与 `rc-chart-1..16` 统一色彩 Token
- [x] 图表色板移除绿色、荧光和彩虹式高冲突组合
- [x] KPI 图标色引用同一 Chart Token,单系列默认品牌红
- [-] 当前 AI 应用没有真实业务图表调用点,未制造测试图表数据
......@@ -38,7 +38,7 @@ Inter 字体文件独立存放于 `public/fonts/`,并由 `.umirc.ts` 注入稳
- Navbar 高 64px,使用 HeroUI Pro `Sidebar.Trigger` 默认图形。
- Navbar 与页面内容共享对称 gutter:桌面 48px、平板 32px、手机 16px。
- Sidebar 用户整行通过 `Dropdown.Trigger` 打开菜单,`退出登录` 调用现有清理流程。
- 应用切换为 HeroUI Modal,46rem/42rem 上限、28px 内容内边距,一列手机/两列 `sm` 以上,应用 identity well 为 36px `Surface tertiary + Avatar soft`。分组必须来自 `/api/info` 的 `site.group`,保持配置顺序并显示真实 `{count} 个应用`;无配置使用“全部应用”,未被配置引用的授权应用使用“其他应用”。
- 应用切换为 HeroUI Modal,46rem/42rem 上限、28px 内容内边距,一列手机/两列 `sm` 以上,应用 identity well 为 36px `Surface default + Avatar soft`。分组必须来自 `/api/info` 的 `site.group`,保持配置顺序并显示真实 `{count} 个应用`;无配置使用“全部应用”,未被配置引用的授权应用使用“其他应用”。
## Surface 与主题
......@@ -47,7 +47,7 @@ Inter 字体文件独立存放于 `public/fonts/`,并由 `.umirc.ts` 注入稳
- 嵌套层 L2:`surface-secondary`,Surface/Card/Modal/Overlay 内控件使用 `secondary`。
- 仅真实第三层静态内容使用 L3 `surface-tertiary`。
- Modal、Dropdown、Popover、Tooltip 等浮层使用独立 `overlay`,不当作第四层 Surface。
- 根节点拥有 `.light/.dark` 与 `data-theme`,Portal 必须继承;主题持久化键为 `xinyuan_color_theme`。
- 根节点固定为 `.light` 与 `data-theme="light"`;页面内容保持浅色,Sidebar 单独消费固定深色 Token,Portal 继承浅色内容主题。
## AI 工作区
......@@ -65,7 +65,7 @@ Inter 字体文件独立存放于 `public/fonts/`,并由 `.umirc.ts` 注入稳
## 本次落地结果
- 权威 `theme.css` SHA-256 为 `a0bf6f243aaf2c94cc8b577145d33d765f8ecab863b3a3b4a358bad4d1a05901`,与 Skill 资产逐字节一致。
- 基准 `theme.css` SHA-256 为 `7eedec7934b9da4575a5a1966621b91bd0319159f46a62b8c6804227d4c9936c`,与 `audit_project_manager` 当前实现逐字节一致。
- Umi 的未使用 `antd` 插件已移除,构建产物不再注入 Ant reset;暗色标题计算为 HeroUI 前景色。
- 页面、业务模块和共享 JSX 的私有视觉 utility 扫描为零;共享外观仅位于权威主题、批准 Tokens 和共享 Shell 设计系统层。
- Ant Design、Ant Design Pro、Ant Design X、Ant X Markdown、antd-style、Headless UI 和 Catalyst UI Kit 活动引用为零;后续开发必须持续保持为零。
......@@ -74,7 +74,13 @@ Inter 字体文件独立存放于 `public/fonts/`,并由 `.umirc.ts` 注入稳
## 消息通知
- `AI 应用` 不在六个完整功能应用白名单内,本次只接入消息通知;不得加入 `shortTitle`、Subject/View 权限或审计材料中心能力。
- Navbar 通知入口直接使用 HeroUI `Badge.Anchor + Badge + Button + Tooltip`,36px Bell 位于主题按钮左侧;Sidebar 不增加通知入口。
- Navbar 通知入口直接使用 HeroUI `Badge.Anchor + Badge + Button + Tooltip`,36px Bell 位于右侧;Sidebar 不增加通知入口。
- `/notifications` 使用 HeroUI Tabs、SearchField、Card、Chip、Button、Toast 和 HeroUI Pro EmptyState;通知分页使用 HeroUI compound Pagination。
- 徽标在未读数为 0 时隐藏,1–99 显示真实值,超过 99 显示 `99+`;按钮可访问名称始终保留真实未读数。
- 通知正文只作为 React 文本渲染,不执行 HTML。站内链接只去除自身 `/ai_chat` base,其他应用路径与外部 HTTP(S) 链接均新窗口打开。
## 图表与品牌色
- 品牌主色固定为 `#DA241A`,HeroUI `--primary`、`--accent`、`--focus` 以及 hover/pressed 状态均使用鑫元红阶。
- 数据可视化统一消费 `--xinyuan-chart-1..16` 与 `--rc-chart-1..16`,按品牌红深浅、暖色、中性色、灰蓝顺序排列,不使用绿色或荧光色。
- KPI/数据卡片图标色映射到同一 `rc-chart-*` Token,单系列默认使用品牌红。
......@@ -3,8 +3,8 @@
## 审计基线
- 审计日期:2026-08-21
- 迁移分支:`codex/ui-ux-xinyuan-migration`
- 视觉与交互唯一基准:已确认的“问题整改”应用
- 迁移分支:`codex/notification-center`
- 视觉与交互唯一基准:`/Users/liuchenlong/Desktop/job/audit_project_manager` 当前实现
- 技术栈:Umi 4、React、JavaScript/JSX、pnpm
- 应用 base/publicPath:`/ai_chat/`
- 未修改 UI 启动地址:`http://localhost:8001/ai_chat/`
......@@ -36,14 +36,14 @@
- 登录:目标与基准桌面均为约 416px Card;36px Logo、40px 字段、24px 字段节奏、验证码与 filled 状态通过。手机均为 326px Card。
- 退出:真实点击用户 Dropdown 的“退出登录”后,`current_user` 为 `null` 并回到登录界面。
- 主题:切换后 `xinyuan_color_theme=dark`,刷新后仍为 dark;Portal 弹窗计算为暗色主题背景与前景。
- 主题:页面内容固定浅色,Sidebar 使用固定深色 Token;Portal 弹窗继承浅色内容主题。
- Sidebar:桌面折叠后内容可见宽度为 0,主区域回到 x=0;移动打开宽 312px,Escape 关闭。
- 应用切换:实际 `/api/application/info` 返回 3 个可访问应用;弹窗并行读取 `/api/info` 的 `site.group`,按后端分组与组内顺序展示真实数量,未配置分组回退“全部应用”,未被分组引用的授权应用回退“其他应用”;桌面 736px、两列、仅 1 个当前状态;手机 358px、单列;Escape 关闭。
- 应用切换专项复验:与 `model_analysis` 在 1280×720 同屏数据下均为 736×484、28px inset、24px radius,Card 内容均为 38px;1440×900、1280×720、768×1024、390×844 的浅/深色共 8 组均无横向溢出,`sm` 起严格两列、手机一列,弹窗自身控制台 0 error / 0 warning。长列表保持 Header 固定且仅 Body 滚动;空分组过滤、站点配置失败、全部应用和其他应用回退均通过内存 fixture 验证,fixture 未写入源码或业务数据。
- 弹窗:审计提示词和 Excel 配置均使用共享 `4xl` HeroUI Modal 尺寸,最大宽度 1088px;手机宽度为视口减 32px,Footer 操作纵向满宽排列。DropZone 打开态、浅/深色和 Escape 已验证。
- Modal 尺寸实测:目标在 1440×900 为 `x=176 / width=1088`,在 3072×1200 仍为 `width=1088`,在 390×844 为 `x=16 / width=358`;“问题整改”基准在 1280×720 为 `width=1088`,在 390×844 同为 `x=16 / width=358`。目标弹窗 `scrollWidth === clientWidth`,页面无横向溢出,控制台 0 error / 0 warning。
- 控制台:生产验收会话为 0 error / 0 warning;八种主题/视口组合均无页面横向溢出。
- 审计知识库操作:`回答规则`、`重新加载`、`更换 Excel` 位于 Navbar 主题按钮左侧;无文件时按业务条件显示 `配置 Excel`。移动端收敛为原生 HeroUI Dropdown。输入区不再重复展示知识库名称、文件名和有效风险点数量。
- 审计知识库操作:`回答规则`、`重新加载`、`更换 Excel` 位于 Navbar 通知按钮左侧;无文件时按业务条件显示 `配置 Excel`。移动端收敛为原生 HeroUI Dropdown。输入区不再重复展示知识库名称、文件名和有效风险点数量。
- 聊天工作区:消息区设置为唯一纵向滚动容器,PromptInput 固定在工作区底部;长消息滚动前后输入框坐标保持 `y=560 / bottom=688`(1280×720)不变。PromptInput 使用官方 stacked/lg 布局,实际高度 128px、正文输入区 72px。
## Inter 字体 HMR 稳定性
......@@ -79,7 +79,7 @@
| 文件/区域 | 当前 UI 所有者 | 主要问题 | 迁移目标 |
| --- | --- | --- | --- |
| `src/layouts/index.jsx` | Ant ConfigProvider + antd-style | 深色蓝青全局主题、渐变、私有圆角和阴影 | HeroUI CSS 顺序、权威 theme、鑫元 Tokens、根级浅/深色 |
| `src/layouts/index.jsx` | Ant ConfigProvider + antd-style | 深色蓝青全局主题、渐变、私有圆角和阴影 | HeroUI CSS 顺序、权威 theme、鑫元 Tokens、固定浅色内容区与深色 Sidebar |
| `src/pages/Home/index.jsx` | Ant Button/Drawer/Dropdown/Modal/Tooltip + Ant X Conversations | 316px 浮动玻璃 Sidebar、icon rail 折叠、无基准 Navbar、页面私有状态 | HeroUI Pro Sidebar/Navbar 真 offcanvas、用户菜单、应用切换、对称 gutter |
| `src/components/LoginModal` | ProForm + Ant Modal | 920px 营销分栏、私有渐变、字段 48px、非基准交互 | 与“问题整改”一致的居中 HeroUI Card 登录页 |
| `src/components/ApplicationSwitcherDialog` | Ant Modal/Alert/Empty/Skeleton | 统计、分组、三列倾向、装饰内容和私有外观 | HeroUI Modal、固定 46rem/42rem、两列应用 Card、单一当前状态 |
......@@ -91,7 +91,7 @@
- 登录 Modal、配置 Modal、Excel 导入 Modal、删除确认 Modal 和应用切换 Modal 均为 Ant Portal,必须迁移到 HeroUI 根并继承根主题。
- 用户菜单和会话操作菜单当前为 Ant Dropdown,必须迁移到 HeroUI Dropdown compound API。
- 移动侧栏当前为 Ant Drawer,必须由 HeroUI Pro Sidebar.Mobile 接管。
- 移动侧栏使用 HeroUI Pro `Sheet` + `Sidebar` 内容,桌面保持真正 offcanvas。
- 文件上传当前包含 Ant Upload/Attachments;活动页面不得保留可见原生 file 控件,最终只允许 DropZone 内部隐藏 input。
- 审计结果包含 Ant Table;若业务数据表仍活动,必须迁移到 HeroUI Pro DataGrid 原生选择/排序/固定列/EmptyState/Pagination 契约。
- 当前活动范围未发现 KPI 或 PieChart/BarChart/ComposedChart 业务数据。禁止为了验收伪造 KPI/图表;若后续配置启用真实数据,应直接使用 HeroUI Pro compound API。
......@@ -116,7 +116,7 @@
- 路由 base/publicPath 为 `/ai_chat/`;新增独立应用内路由 `/notifications`,浏览器地址为 `/ai_chat/notifications`。
- 登录状态沿用 `localStorage.current_user`,Navbar 与通知页不读取、复制或合并其他应用的 `user.applications[]` 权限。
- 原 Shell 的页面操作由 `useNavbarActions` 注入;通知计数提升到 `AppShell`,登录用户变化、通知状态事件及 60 秒定时器触发刷新。
- Sidebar 保留原会话和用户数据流,不增加通知入口。Navbar 的通知入口位于页面操作与主题按钮之间。
- Sidebar 保留原会话和用户数据流,不增加通知入口。Navbar 的通知入口位于页面操作右侧。
- 通知接口沿用平台协议:`POST /notice/customer/query`、`PUT /notice/customer/read`、`GET /notice/customer/count`、`PUT /notice/customer/{action}/action/{method}`;目标项目请求层继续统一添加 `/api` 前缀。
- 查询固定每页 10 条,搜索 300ms 防抖并转义正则,未读筛选发送 `status: "unread"`;动态 action 只提交 `{notice_id}`。
- 现有目标项目无 Subject/View、record-workbench、RecordDetailDrawer、材料中心文件字段或通知模块;未新增相关能力,也未改变登录、会话和 AI 业务回调。
......
......@@ -31,6 +31,8 @@
"@umijs/max": "^4.4.12",
"clsx": "^2.1.1",
"dayjs": "^1.11.18",
"echarts": "^6.1.0",
"echarts-for-react": "^3.0.6",
"embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"jszip": "^3.10.1",
......
......@@ -70,6 +70,12 @@ importers:
dayjs:
specifier: ^1.11.18
version: 1.11.21
echarts:
specifier: ^6.1.0
version: 6.1.0
echarts-for-react:
specifier: ^3.0.6
version: 3.0.6(echarts@6.1.0)(react@19.2.8)
embla-carousel:
specifier: ^8.6.0
version: 8.6.0
......@@ -4064,6 +4070,15 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
echarts-for-react@3.0.6:
resolution: {integrity: sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==}
peerDependencies:
echarts: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
react: ^15.0.0 || >=16.0.0
echarts@6.1.0:
resolution: {integrity: sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==}
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
......@@ -7840,6 +7855,9 @@ packages:
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
size-sensor@1.0.3:
resolution: {integrity: sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==}
slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
......@@ -8304,6 +8322,9 @@ packages:
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
tslib@2.4.0:
resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==}
......@@ -8739,6 +8760,9 @@ packages:
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
zrender@6.1.0:
resolution: {integrity: sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==}
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
......@@ -13465,6 +13489,18 @@ snapshots:
eastasianwidth@0.2.0: {}
echarts-for-react@3.0.6(echarts@6.1.0)(react@19.2.8):
dependencies:
echarts: 6.1.0
fast-deep-equal: 3.1.3
react: 19.2.8
size-sensor: 1.0.3
echarts@6.1.0:
dependencies:
tslib: 2.3.0
zrender: 6.1.0
ee-first@1.1.1: {}
electron-to-chromium@1.5.401: {}
......@@ -18135,6 +18171,8 @@ snapshots:
sisteransi@1.0.5: {}
size-sensor@1.0.3: {}
slash@3.0.0: {}
slash@4.0.0: {}
......@@ -18687,6 +18725,8 @@ snapshots:
tslib@1.14.1: {}
tslib@2.3.0: {}
tslib@2.4.0: {}
tslib@2.8.1: {}
......@@ -19210,4 +19250,8 @@ snapshots:
zod@4.4.3: {}
zrender@6.1.0:
dependencies:
tslib: 2.3.0
zwitch@2.0.4: {}
import '@/tailwind.css';
import { defineApp, matchRoutes } from 'umi';
import { THEME_STORAGE_KEY } from './config/application';
import { createTracking } from './services/DataService';
import {
collectAppInfo,
......@@ -9,14 +8,12 @@ import {
} from './utils/trackingHelper';
if (typeof document !== 'undefined') {
const storedTheme = window.localStorage?.getItem?.(THEME_STORAGE_KEY);
const activeTheme = storedTheme === 'dark' ? 'dark' : 'light';
const root = document.documentElement;
root.classList.toggle('dark', activeTheme === 'dark');
root.classList.toggle('light', activeTheme === 'light');
root.dataset.theme = activeTheme;
root.style.colorScheme = activeTheme;
root.classList.remove('dark');
root.classList.add('light');
root.dataset.theme = 'light';
root.style.colorScheme = 'light';
}
export const request = {
......
......@@ -6,12 +6,10 @@ import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
import EditOutlined from '@ant-design/icons/EditOutlined';
import ExportOutlined from '@ant-design/icons/ExportOutlined';
import LogoutOutlined from '@ant-design/icons/LogoutOutlined';
import MoonOutlined from '@ant-design/icons/MoonOutlined';
import MoreOutlined from '@ant-design/icons/MoreOutlined';
import PlusOutlined from '@ant-design/icons/PlusOutlined';
import PushpinOutlined from '@ant-design/icons/PushpinOutlined';
import SearchOutlined from '@ant-design/icons/SearchOutlined';
import SunOutlined from '@ant-design/icons/SunOutlined';
import WarningOutlined from '@ant-design/icons/WarningOutlined';
function withSize(Icon) {
......@@ -35,12 +33,10 @@ export const Edit = withSize(EditOutlined);
export const OpenExternal = withSize(ExportOutlined);
export const ExclamationTriangleIcon = withSize(WarningOutlined);
export const LogOut = withSize(LogoutOutlined);
export const Moon = withSize(MoonOutlined);
export const More = withSize(MoreOutlined);
export const Plus = withSize(PlusOutlined);
export const Pin = withSize(PushpinOutlined);
export const Search = withSize(SearchOutlined);
export const Sun = withSize(SunOutlined);
export function SidebarPanel({ size = 16, style, ...props }) {
return (
......
import { THEME_STORAGE_KEY } from '@/config/application';
import {
loadUnreadNotificationCount,
NOTIFICATION_STATE_EVENT,
......@@ -31,18 +30,6 @@ export function useNavbarActions(actions) {
}, [actions, setNavbarActions]);
}
function getInitialTheme() {
if (typeof window === 'undefined') return 'light';
try {
return window.localStorage.getItem(THEME_STORAGE_KEY) === 'dark'
? 'dark'
: 'light';
} catch {
return 'light';
}
}
export default function AppShell({
children,
open,
......@@ -50,24 +37,9 @@ export default function AppShell({
title,
...sidebarProps
}) {
const [theme, setTheme] = useState(getInitialTheme);
const [navbarActions, setNavbarActions] = useState(null);
const [notificationCount, setNotificationCount] = useState(0);
useLayoutEffect(() => {
const root = document.documentElement;
root.classList.toggle('dark', theme === 'dark');
root.classList.toggle('light', theme === 'light');
root.dataset.theme = theme;
root.style.colorScheme = theme;
try {
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
} catch {
// The theme still applies for this session when storage is unavailable.
}
}, [theme]);
useEffect(() => {
if (!sidebarProps.userInfo) {
setNotificationCount(0);
......@@ -101,20 +73,19 @@ export default function AppShell({
return (
<NavbarActionsContext.Provider value={setNavbarActions}>
<Sidebar.Provider
className={`template-dashboard-shell rc-app-shell ${theme}`}
className="template-dashboard-shell rc-app-shell light"
collapsible="offcanvas"
navigate={(href) => history.push(href)}
open={open}
onOpenChange={onOpenChange}
>
<DashboardSidebar theme={theme} {...sidebarProps} />
<DashboardSidebar {...sidebarProps} />
<Sidebar.Main className="app-main-glass rc-app-main">
<DashboardNavbar
actions={navbarActions}
notificationCount={notificationCount}
theme={theme}
title={title}
onOpenNotifications={() => history.push('/notifications')}
onThemeChange={setTheme}
/>
<div className="app-shell-content-scroll min-h-0 flex-1 overflow-y-auto">
<div className="rc-app-page">{children}</div>
......
import { ExclamationTriangleIcon } from '@/components/AppIcons';
import xinyuanLogo from '@/assets/xy-logo-red.png';
import { ExclamationTriangleIcon, Search } from '@/components/AppIcons';
import {
APPLICATION_DISPLAY_NAME,
APPLICATION_NAME,
} from '@/config/application';
import { getApplications, getSystemInfo } from '@/services/DataService';
import { EmptyState } from '@heroui-pro/react/empty-state';
import { ItemCard } from '@heroui-pro/react/item-card';
import { ItemCardGroup } from '@heroui-pro/react/item-card-group';
import {
Alert,
Avatar,
Card,
Chip,
Description,
Modal,
ScrollShadow,
SearchField,
Skeleton,
Surface,
} from '@heroui/react';
......@@ -34,7 +41,7 @@ function normalizeApplication(application, fallback = {}) {
fallback?.title ||
application?.title ||
'进入对应应用继续处理业务',
logo: application?.logo || fallback?.logo || null,
logo: application?.logo || fallback?.logo || xinyuanLogo,
url: resolveApplicationUrl({ name }),
order: application?.order ?? fallback?.order ?? 0,
};
......@@ -83,6 +90,15 @@ function mergeApplications(fetchedApplications, fallbackApplications) {
}
});
const currentApplication = mergedMap.get(APPLICATION_NAME);
mergedMap.set(
APPLICATION_NAME,
normalizeApplication(
{ name: APPLICATION_NAME },
currentApplication || { title: APPLICATION_DISPLAY_NAME },
),
);
return Array.from(mergedMap.values()).sort((left, right) => {
const orderDifference = Number(left.order || 0) - Number(right.order || 0);
return orderDifference || left.title.localeCompare(right.title, 'zh-CN');
......@@ -133,6 +149,7 @@ function buildApplicationSections(applications, applicationGroups) {
}
function isCurrentApplication(application) {
if (application?.name === APPLICATION_NAME) return true;
if (typeof window === 'undefined') return false;
const currentPath = window.location.pathname;
......@@ -146,76 +163,85 @@ function isCurrentApplication(application) {
}
function ApplicationCard({ application, current }) {
const initial = application.title.trim().slice(0, 1).toUpperCase() || 'A';
const description = application.description?.trim();
const hasDistinctDescription =
description && description !== application.title.trim();
const target = application.url?.startsWith('http') ? '_blank' : undefined;
return (
<Card className="h-full" variant="secondary">
<Card.Content>
<a
aria-current={current ? 'page' : undefined}
className="flex min-w-0 items-center gap-3 no-underline"
href={application.url}
>
<Surface
aria-hidden="true"
className="rc-application-identity-well"
variant="tertiary"
<ItemCard
aria-current={current ? 'page' : undefined}
className="h-full min-w-0 no-underline"
href={application.url}
rel={target ? 'noreferrer' : undefined}
render={(props) => <a {...props} />}
target={target}
variant={current ? 'outline' : 'default'}
>
<ItemCard.Icon aria-hidden="true">
<Surface variant="default" className="rc-application-identity-well">
<Avatar
className="rc-application-identity-avatar"
color="accent"
variant="soft"
>
<Avatar
className="rc-application-identity-avatar"
color="accent"
variant="soft"
>
{application.logo ? (
<Avatar.Image
alt=""
className="object-contain p-1.5"
src={application.logo}
/>
) : null}
<Avatar.Fallback>{initial}</Avatar.Fallback>
</Avatar>
</Surface>
<span className="min-w-0 flex-1">
<span
className="block truncate text-sm font-medium leading-5"
title={application.title}
>
{application.title}
</span>
<Description
className="mt-0.5 block truncate leading-4"
title={application.description}
>
{application.description || application.name || '应用'}
</Description>
</span>
{current ? (
<Chip color="accent" size="sm">
当前
</Chip>
) : null}
</a>
</Card.Content>
</Card>
<Avatar.Image
alt=""
className="object-contain p-1.5"
src={application.logo}
/>
<Avatar.Fallback>
<Avatar.Image
alt=""
className="object-contain p-1.5"
src={application.logo}
/>
</Avatar.Fallback>
</Avatar>
</Surface>
</ItemCard.Icon>
<ItemCard.Content>
<ItemCard.Title title={application.title}>
{application.title}
</ItemCard.Title>
{hasDistinctDescription ? (
<ItemCard.Description title={description}>
{description}
</ItemCard.Description>
) : null}
</ItemCard.Content>
{current ? (
<ItemCard.Action>
<Chip color="accent" size="sm">
当前
</Chip>
</ItemCard.Action>
) : null}
</ItemCard>
);
}
function LoadingSkeleton() {
return (
<div aria-label="正在加载应用" className="grid gap-3 sm:grid-cols-2">
<ItemCardGroup
aria-label="正在加载应用"
className="grid-cols-1 sm:grid-cols-2"
columns={2}
layout="grid"
variant="transparent"
>
{Array.from({ length: 6 }).map((_, index) => (
<Card key={index} variant="secondary">
<Card.Content className="flex items-center gap-3">
<ItemCard key={index} variant="default">
<ItemCard.Icon>
<Skeleton className="size-9" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-3 w-full" />
</div>
</Card.Content>
</Card>
</ItemCard.Icon>
<ItemCard.Content className="gap-2">
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-3 w-full" />
</ItemCard.Content>
</ItemCard>
))}
</div>
</ItemCardGroup>
);
}
......@@ -232,6 +258,7 @@ export default function ApplicationSwitcherDialog({ open, onClose, userInfo }) {
fallbackApplications.length > 0 ? 'success' : 'idle',
);
const [errorMessage, setErrorMessage] = useState('');
const [applicationSearch, setApplicationSearch] = useState('');
const currentApplicationName = useMemo(
() => applications.find(isCurrentApplication)?.name || '',
[applications],
......@@ -240,9 +267,29 @@ export default function ApplicationSwitcherDialog({ open, onClose, userInfo }) {
() => buildApplicationSections(applications, siteInfo?.group),
[applications, siteInfo?.group],
);
const filteredApplicationSections = useMemo(() => {
const keyword = applicationSearch.trim().toLocaleLowerCase();
if (!keyword) return applicationSections;
return applicationSections
.map((section) => ({
...section,
applications: section.applications.filter((application) =>
[application.title, application.description, application.name].some(
(value) =>
String(value || '')
.toLocaleLowerCase()
.includes(keyword),
),
),
}))
.filter((section) => section.applications.length > 0);
}, [applicationSearch, applicationSections]);
useEffect(() => {
if (!open) return;
if (!open) {
setApplicationSearch('');
return;
}
let cancelled = false;
const loadDialogData = async () => {
......@@ -301,46 +348,71 @@ export default function ApplicationSwitcherDialog({ open, onClose, userInfo }) {
>
<Modal.Dialog
aria-label="应用切换"
className="min-w-0 max-h-[min(42rem,calc(100dvh-2rem))] p-7"
className="min-w-0 max-h-[min(42rem,calc(100dvh-2rem))] bg-background p-7"
style={{
width: 'min(46rem, calc(100vw - 2rem))',
maxWidth: 'min(46rem, calc(100vw - 2rem))',
}}
>
<Modal.CloseTrigger
aria-label="关闭"
style={{ insetInlineEnd: '1.75rem', top: '1.75rem' }}
/>
<Modal.Header>
<Modal.Heading>应用切换</Modal.Heading>
<div className="text-sm text-muted">选择你可访问的应用</div>
<div className="flex w-full min-w-0 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<SearchField
aria-label="搜索应用"
className="w-full min-w-0 sm:flex-1 sm:w-auto"
value={applicationSearch}
variant="secondary"
onChange={setApplicationSearch}
>
<SearchField.Group>
<SearchField.SearchIcon>
<Search size={16} />
</SearchField.SearchIcon>
<SearchField.Input placeholder="搜索应用" />
<SearchField.ClearButton aria-label="清空应用搜索" />
</SearchField.Group>
</SearchField>
</div>
</Modal.Header>
<Modal.Body style={{ marginTop: '0.75rem' }}>
{status === 'error' && errorMessage ? (
<Alert status="warning">
<Alert.Indicator>
<ExclamationTriangleIcon size={20} />
</Alert.Indicator>
<Alert.Content>
<Alert.Description>{errorMessage}</Alert.Description>
</Alert.Content>
</Alert>
) : null}
{status === 'loading' && applications.length === 0 ? (
<LoadingSkeleton />
) : applications.length > 0 ? (
<div className="space-y-6">
{applicationSections.map((section) => (
<section key={section.key}>
<div className="mb-3 flex items-center justify-between gap-3">
<h3 className="text-sm font-medium leading-5">
{section.label}
</h3>
<Description>
{section.applications.length} 个应用
</Description>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<Modal.Body
className="flex min-h-0 overflow-hidden"
style={{ marginTop: '0.75rem' }}
>
<ScrollShadow
className="h-full min-h-0 flex-1 overflow-y-auto pe-1 pb-1"
hideScrollBar={false}
offset={12}
size={24}
>
{status === 'error' && errorMessage ? (
<Alert className="mb-4" status="warning">
<Alert.Indicator>
<ExclamationTriangleIcon size={20} />
</Alert.Indicator>
<Alert.Content>
<Alert.Description>{errorMessage}</Alert.Description>
</Alert.Content>
</Alert>
) : null}
{status === 'loading' && applications.length === 0 ? (
<LoadingSkeleton />
) : filteredApplicationSections.length > 0 ? (
<div className="space-y-6">
{filteredApplicationSections.map((section) => (
<ItemCardGroup
key={section.key}
className="grid-cols-1 sm:grid-cols-2"
columns={2}
layout="grid"
variant="transparent"
>
<ItemCardGroup.Header className="flex items-center justify-between gap-3">
<ItemCardGroup.Title>
{section.label}
</ItemCardGroup.Title>
<ItemCardGroup.Description className="mt-0">
{section.applications.length} 个应用
</ItemCardGroup.Description>
</ItemCardGroup.Header>
{section.applications.map((application) => (
<ApplicationCard
application={application}
......@@ -350,20 +422,26 @@ export default function ApplicationSwitcherDialog({ open, onClose, userInfo }) {
key={application.name}
/>
))}
</div>
</section>
))}
</div>
) : (
<EmptyState size="md">
<EmptyState.Header>
<EmptyState.Title>暂无可切换应用</EmptyState.Title>
<EmptyState.Description>
当前账号还没有分配其它应用权限。
</EmptyState.Description>
</EmptyState.Header>
</EmptyState>
)}
</ItemCardGroup>
))}
</div>
) : (
<EmptyState size="md">
<EmptyState.Header>
<EmptyState.Title>
{applicationSearch.trim()
? '没有找到匹配的应用'
: '暂无可切换应用'}
</EmptyState.Title>
<EmptyState.Description>
{applicationSearch.trim()
? '请尝试其他关键词。'
: '当前账号还没有分配其它应用权限。'}
</EmptyState.Description>
</EmptyState.Header>
</EmptyState>
)}
</ScrollShadow>
</Modal.Body>
</Modal.Dialog>
</Modal.Container>
......
import { Navbar } from '@heroui-pro/react/navbar';
import { Sidebar, useSidebar } from '@heroui-pro/react/sidebar';
import { Badge, Button, Tooltip } from '@heroui/react';
import { Bell, Moon, SidebarPanel, Sun } from './AppIcons';
import { Bell, SidebarPanel } from './AppIcons';
export default function DashboardNavbar({
actions,
notificationCount = 0,
onOpenNotifications,
onThemeChange,
theme,
title,
}) {
const { isMobile, isOpen } = useSidebar();
const isDark = theme === 'dark';
const themeLabel = isDark ? '切换至浅色模式' : '切换至深色模式';
const normalizedNotificationCount = Math.max(
0,
Number(notificationCount) || 0,
......@@ -61,20 +57,6 @@ export default function DashboardNavbar({
</Badge.Anchor>
<Tooltip.Content>通知消息</Tooltip.Content>
</Tooltip>
<Tooltip delay={0}>
<Button
isIconOnly
aria-label={themeLabel}
aria-pressed={isDark}
className="app-navbar-icon-button rc-navbar-icon-button"
size="sm"
variant="ghost"
onPress={() => onThemeChange(isDark ? 'light' : 'dark')}
>
{isDark ? <Sun size={16} /> : <Moon size={16} />}
</Button>
<Tooltip.Content>{themeLabel}</Tooltip.Content>
</Tooltip>
</div>
</Navbar.Header>
</Navbar>
......
import xyLogoRed from '@/assets/xy-logo-red.png';
import xyLogoWhite from '@/assets/xy-logo-white.png';
import { APPLICATION_DISPLAY_NAME } from '@/config/application';
import { Sidebar } from '@heroui-pro/react/sidebar';
import { Avatar, Button, Dropdown, Label, Tooltip } from '@heroui/react';
import { Sheet } from '@heroui-pro/react';
import { Sidebar, useSidebar } from '@heroui-pro/react/sidebar';
import { Avatar, Button, Dropdown, Label } from '@heroui/react';
import {
AppGrid,
Comment,
......@@ -127,16 +127,7 @@ function SidebarContents({
<Sidebar.Header>
<div className="app-sidebar-brand rc-sidebar-brand">
<div className="app-sidebar-brand-mark rc-sidebar-brand-mark">
<img
alt="鑫元"
className="rc-sidebar-logo rc-sidebar-logo--light"
src={xyLogoRed}
/>
<img
alt="鑫元"
className="rc-sidebar-logo rc-sidebar-logo--dark"
src={xyLogoWhite}
/>
<img alt="鑫元" className="rc-sidebar-logo" src={xyLogoWhite} />
</div>
<div
className="app-sidebar-brand-main rc-sidebar-brand-main"
......@@ -150,19 +141,16 @@ function SidebarContents({
className="app-sidebar-app-switcher rc-sidebar-app-switcher"
data-sidebar="label"
>
<Tooltip delay={0}>
<Button
isIconOnly
aria-label="切换应用"
className="app-sidebar-app-trigger rc-sidebar-app-trigger"
size="sm"
variant="ghost"
onPress={onOpenApplicationSwitcher}
>
<AppGrid size={16} />
</Button>
<Tooltip.Content>切换应用</Tooltip.Content>
</Tooltip>
<Button
isIconOnly
aria-label="切换应用"
className="app-sidebar-app-trigger rc-sidebar-app-trigger"
size="sm"
variant="ghost"
onPress={onOpenApplicationSwitcher}
>
<AppGrid size={16} />
</Button>
</div>
</div>
</Sidebar.Header>
......@@ -230,17 +218,38 @@ function SidebarContents({
}
export default function DashboardSidebar(props) {
const mobile = useSidebar();
function AccessibleMobileSidebar() {
if (!mobile.isMobile) return null;
return (
<Sheet.Root
isOpen={mobile.isMobileOpen}
placement={mobile.side}
onOpenChange={mobile.setMobileOpen}
>
<Sheet.Backdrop variant="blur">
<Sheet.Content className="sidebar__mobile-sheet">
<Sheet.Dialog
aria-label={`${APPLICATION_DISPLAY_NAME}导航`}
className="sidebar__mobile-dialog"
>
<div className="sidebar__mobile app-sidebar-glass rc-mobile-sidebar">
<SidebarContents {...props} />
</div>
</Sheet.Dialog>
</Sheet.Content>
</Sheet.Backdrop>
</Sheet.Root>
);
}
return (
<>
<Sidebar className="app-sidebar-glass rc-sidebar">
<SidebarContents {...props} />
</Sidebar>
<Sidebar.Mobile className="app-sidebar-glass rc-mobile-sidebar">
<h2 className="sr-only" slot="title">
{APPLICATION_DISPLAY_NAME}导航
</h2>
<SidebarContents {...props} />
</Sidebar.Mobile>
<AccessibleMobileSidebar />
</>
);
}
export const APPLICATION_DISPLAY_NAME = 'AI 应用';
export const APPLICATION_NAME = 'ai_chat';
export const APPLICATION_BASE_PATH = '/ai_chat/';
export const THEME_STORAGE_KEY = 'xinyuan_color_theme';
export const ROUTE_TITLES = {
'/': APPLICATION_DISPLAY_NAME,
......
......@@ -435,6 +435,41 @@
gap: var(--rc-page-module-gap);
}
.rc-section-title {
margin: 0;
color: var(--rc-text-primary);
font-size: 1.125rem;
font-weight: var(--rc-font-bold);
line-height: 1.5rem;
letter-spacing: 0;
}
.rc-module-title {
margin: 0;
color: var(--rc-text-primary);
font-size: 1rem;
font-weight: var(--rc-font-semibold);
line-height: 1.5rem;
letter-spacing: 0;
}
.rc-modal-header-actions {
align-self: flex-start;
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.5rem;
margin-inline-start: auto;
max-width: 100%;
}
@media (max-width: 40rem) {
.rc-modal-header-actions {
flex-basis: 100%;
}
}
.rc-page-grid {
min-width: 0;
gap: var(--rc-page-module-gap);
......@@ -453,7 +488,7 @@
grid-column: 1 / -1;
}
/* Recharts keeps keyboard navigation while pointer focus stays visually quiet. */
/* Charts keep keyboard navigation while pointer focus stays visually quiet. */
.rc-chart-interaction:focus:not(:focus-visible),
.rc-chart-interaction :focus:not(:focus-visible) {
outline: none;
......@@ -465,6 +500,10 @@
outline-offset: 2px;
}
.rc-echarts-tooltip {
position: fixed !important;
}
.rc-kpi-grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
......@@ -509,10 +548,33 @@
box-shadow: var(--rc-shadow-card);
}
.rc-filter-surface {
border-radius: var(--rc-radius-overlay);
}
.rc-dashboard-card {
border-radius: 1.5rem;
}
.rc-workspace-module {
border-radius: 1.5rem;
border: 1px solid var(--border);
background: transparent;
box-shadow: none;
}
.rc-empty-state-message {
color: var(--rc-text-empty);
font-size: var(--text-sm);
font-weight: var(--font-weight-normal);
}
.rc-empty-field-text {
color: var(--rc-text-empty);
font-size: var(--text-sm);
font-weight: var(--font-weight-normal);
}
/* HeroUI Pro KPI.Icon keeps its compound role; this class supplies Xinyuan tones. */
.rc-metric-icon-surface {
--rc-metric-icon-color: var(--rc-metric-tone-neutral);
......@@ -656,6 +718,122 @@
gap: var(--rc-list-stack-gap);
}
/* Keep long data sets inside the table while the column headings remain visible. */
.rc-data-table-sticky .data-table-scroll {
position: relative;
max-height: min(60dvh, 42rem);
overflow-x: auto;
overflow-y: auto;
overscroll-behavior: contain;
}
/* DataGrid adds a non-scrolling resize wrapper. Let sticky headers use the
outer scroll container as their containing block instead of this wrapper. */
.rc-data-table-sticky .table__resizable-container {
overflow: visible;
}
.rc-data-table-sticky [data-slot='table-header'] .table__column {
position: sticky;
top: 0;
inset-block-start: 0;
z-index: 4;
display: table-cell;
color: var(--muted);
background: var(--surface-secondary);
box-shadow: inset 0 -1px 0 var(--separator);
}
.rc-data-table-sticky [data-slot='table-header'] .table__column.bg-accent-soft {
background: var(--accent-soft);
color: var(--accent-soft-foreground);
}
/* The leading pinned column must sit above scrolling body cells. Keep its
surface opaque so text from adjacent columns cannot bleed through while
horizontal or vertical scrolling. */
.rc-data-table-sticky [data-pinned='start'] {
position: sticky;
z-index: 6;
background: var(--surface);
}
.rc-data-table-sticky [data-slot='table-header'] [data-pinned='start'] {
z-index: 8;
background: var(--surface-secondary);
}
/* Mirror HeroUI's row states on the opaque pinned surface so the leading
column still responds to hover and selection instead of looking detached. */
@media (hover: hover) {
.rc-data-table-sticky
[data-slot='data-grid']
.table__row:hover
[data-pinned='start'].table__cell,
.rc-data-table-sticky
[data-slot='data-grid']
.table__row[data-hovered='true']
[data-pinned='start'].table__cell {
/* Keep the pinned cell opaque; transparent hover fills let scrolled
sibling-cell text show through the fixed column. */
background-color: var(--color-surface-secondary);
}
@supports (color: color-mix(in oklab, white, black)) {
.rc-data-table-sticky
[data-slot='data-grid']
.table__row:hover
[data-pinned='start'].table__cell,
.rc-data-table-sticky
[data-slot='data-grid']
.table__row[data-hovered='true']
[data-pinned='start'].table__cell {
background-color: color-mix(
in oklab,
var(--color-surface) 40%,
var(--color-surface-secondary)
);
}
}
}
.rc-data-table-sticky
[data-slot='data-grid']
.table__row[data-selected='true']
[data-pinned='start'].table__cell {
background-color: var(--color-surface-secondary);
}
@supports (color: color-mix(in oklab, white, black)) {
.rc-data-table-sticky
[data-slot='data-grid']
.table__row[data-selected='true']
[data-pinned='start'].table__cell {
background-color: color-mix(
in oklab,
var(--color-surface) 10%,
var(--color-surface-secondary)
);
}
}
@media (max-width: 48rem) {
.rc-data-table-sticky .data-table-scroll {
max-height: min(54dvh, 32rem);
}
}
@media (min-width: 769px) {
.action-bar:has(.rc-data-grid-action-bar) {
inset-inline: 0;
}
body:has(.rc-app-shell[data-state='expanded'])
.action-bar:has(.rc-data-grid-action-bar) {
inset-inline-start: var(--rc-sidebar-width);
}
}
@media (min-width: 80rem) {
.rc-chart-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
......@@ -847,23 +1025,6 @@
opacity: 0.6;
}
.rc-application-identity-well {
display: grid;
place-items: center;
width: 2.25rem;
height: 2.25rem;
flex: none;
overflow: hidden;
border-radius: var(--rc-radius-control);
}
.rc-application-identity-avatar.avatar {
width: 100%;
height: 100%;
border-radius: var(--rc-radius-control);
background: transparent;
}
.rc-sidebar-app-logo,
.rc-sidebar-app-initial {
width: 2rem;
......@@ -881,6 +1042,23 @@
font-weight: var(--rc-font-semibold);
}
.rc-application-identity-well {
display: grid;
width: 2.25rem;
height: 2.25rem;
flex: none;
place-items: center;
overflow: hidden;
border-radius: var(--rc-radius-control);
}
.rc-application-identity-avatar {
width: 100%;
height: 100%;
border-radius: var(--rc-radius-control);
background: transparent;
}
.rc-app-shell .sidebar__content,
.rc-mobile-sidebar .sidebar__content {
min-height: 0;
......@@ -1204,13 +1382,13 @@
place-items: center;
padding: 0;
border-radius: 50%;
color: var(--foreground);
color: var(--rc-sidebar-foreground);
background-color: transparent;
line-height: 1;
--button-bg-hover: var(--accent-soft);
--button-bg-pressed: var(--accent-soft);
--button-fg: var(--foreground);
--button-bg-hover: var(--rc-sidebar-item-selected);
--button-bg-pressed: var(--rc-sidebar-item-selected);
--button-fg: var(--rc-sidebar-item-selected-foreground);
}
.app-sidebar-app-trigger .anticon {
......@@ -1230,16 +1408,16 @@
@media (hover: hover) {
.app-sidebar-app-trigger.button:hover,
.app-sidebar-app-trigger.button[data-hovered='true'] {
color: var(--foreground);
background-color: var(--accent-soft);
color: var(--rc-sidebar-item-selected-foreground);
background-color: var(--rc-sidebar-item-selected);
}
}
.app-sidebar-app-trigger.button:active,
.app-sidebar-app-trigger.button[data-pressed='true'],
.app-sidebar-app-trigger.button[aria-expanded='true'] {
color: var(--foreground);
background-color: var(--accent-soft);
color: var(--rc-sidebar-item-selected-foreground);
background-color: var(--rc-sidebar-item-selected);
}
.app-sidebar-app-dialog {
......@@ -1247,81 +1425,6 @@
outline: none;
}
.rc-dialog-panel--xs,
.rc-dialog-panel--sm,
.rc-dialog-panel--md,
.rc-dialog-panel--lg,
.rc-dialog-panel--xl,
.rc-dialog-panel--2xl,
.rc-dialog-panel--3xl,
.rc-dialog-panel--4xl,
.rc-dialog-panel--5xl {
width: min(calc(100vw - 5rem), var(--rc-dialog-panel-width));
max-width: var(--rc-dialog-panel-width);
}
.rc-dialog-panel--xs {
--rc-dialog-panel-width: var(--rc-dialog-width-xs);
}
.rc-dialog-panel--sm {
--rc-dialog-panel-width: var(--rc-dialog-width-sm);
}
.rc-dialog-panel--md {
--rc-dialog-panel-width: var(--rc-dialog-width-md);
}
.rc-dialog-panel--lg {
--rc-dialog-panel-width: var(--rc-dialog-width-lg);
}
.rc-dialog-panel--xl {
--rc-dialog-panel-width: var(--rc-dialog-width-xl);
}
.rc-dialog-panel--2xl {
--rc-dialog-panel-width: var(--rc-dialog-width-2xl);
}
.rc-dialog-panel--3xl {
--rc-dialog-panel-width: var(--rc-dialog-width-3xl);
}
.rc-dialog-panel--4xl {
--rc-dialog-panel-width: var(--rc-dialog-width-4xl);
}
.rc-dialog-panel--5xl {
--rc-dialog-panel-width: var(--rc-dialog-width-5xl);
}
@media (max-width: 640px) {
.rc-dialog-panel--xs,
.rc-dialog-panel--sm,
.rc-dialog-panel--md,
.rc-dialog-panel--lg,
.rc-dialog-panel--xl,
.rc-dialog-panel--2xl,
.rc-dialog-panel--3xl,
.rc-dialog-panel--4xl,
.rc-dialog-panel--5xl {
width: calc(100vw - 2rem);
max-width: calc(100vw - 2rem);
}
}
@media (max-width: 767px) {
.rc-dialog-footer.modal__footer {
flex-direction: column-reverse;
align-items: stretch;
}
.rc-dialog-footer.modal__footer > .button {
width: 100%;
}
}
.app-sidebar-app-scroll {
max-height: min(32rem, calc(100dvh - 7rem));
padding: 1.5rem;
......@@ -1498,3 +1601,91 @@
border-right: 0;
}
}
/* Fixed split-shell theme: dark navigation, permanently light content canvas. */
.template-dashboard-shell .app-sidebar-glass,
.rc-mobile-sidebar.app-sidebar-glass {
color: var(--rc-sidebar-foreground);
border-color: var(--rc-sidebar-border) !important;
background-color: var(--rc-sidebar-background) !important;
}
.template-dashboard-shell
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content,
.rc-mobile-sidebar
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content {
background: var(--rc-sidebar-item-selected);
}
.template-dashboard-shell
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content,
.template-dashboard-shell
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content
*,
.rc-mobile-sidebar
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content,
.rc-mobile-sidebar
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content
* {
color: var(--rc-sidebar-item-selected-foreground) !important;
}
.template-dashboard-shell .sidebar__menu-item:hover .sidebar__menu-item-content,
.template-dashboard-shell
.sidebar__menu-item[data-hovered='true']
.sidebar__menu-item-content,
.rc-mobile-sidebar .sidebar__menu-item:hover .sidebar__menu-item-content,
.rc-mobile-sidebar
.sidebar__menu-item[data-hovered='true']
.sidebar__menu-item-content,
.app-sidebar-user-trigger.dropdown__trigger:hover,
.app-sidebar-user-trigger.dropdown__trigger[data-hovered='true'] {
background: var(--rc-sidebar-item-hover);
}
.app-sidebar-user-trigger.dropdown__trigger[aria-expanded='true'],
.app-sidebar-app-trigger.button[data-pressed='true'],
.app-sidebar-app-trigger.button[aria-expanded='true'] {
background: var(--rc-sidebar-item-selected);
}
.app-sidebar-user-avatar,
.app-sidebar-app-trigger.button,
.app-sidebar-user-trigger.dropdown__trigger {
color: var(--rc-sidebar-foreground);
background: transparent;
}
.app-sidebar-user-avatar {
background: var(--rc-sidebar-item-hover);
}
.template-dashboard-shell .sidebar__group-label,
.rc-mobile-sidebar .sidebar__group-label,
.template-dashboard-shell .sidebar__menu-icon,
.rc-mobile-sidebar .sidebar__menu-icon,
.app-sidebar-user-description {
color: var(--rc-sidebar-muted);
}
.template-dashboard-shell .sidebar__menu-label,
.rc-mobile-sidebar .sidebar__menu-label,
.app-sidebar-title,
.app-sidebar-user-name,
.app-sidebar-app-trigger.button {
color: var(--rc-sidebar-foreground);
}
.app-sidebar-user-trigger.dropdown__trigger:focus-visible,
.app-sidebar-app-trigger.button:focus-visible,
.template-dashboard-shell .sidebar__menu-item:focus-visible,
.rc-mobile-sidebar .sidebar__menu-item:focus-visible {
outline: 2px solid var(--rc-sidebar-focus);
outline-offset: 2px;
}
......@@ -11,39 +11,57 @@
[data-theme='light'],
[data-theme='default'] {
/* Theme Colors (Light Mode) */
--accent: oklch(45.32% 0.1661 27.03);
/* Xinyuan Fund brand red. HeroUI's accent is the primary action color. */
--accent: #da241a;
--accent-hover: #c91f17;
--accent-pressed: #b51b14;
--accent-foreground: oklch(99.11% 0 0);
--background: oklch(97.02% 0.0045 27.03);
--border: oklch(90% 0.0045 27.03);
--danger: oklch(45.32% 0.1661 27.03);
--primary: #da241a;
--primary-hover: #c91f17;
--primary-pressed: #b51b14;
--primary-foreground: #fff;
--primary-50: #fce9e7;
--primary-100: #f9d0cc;
--primary-200: #f3a29a;
--primary-300: #ec756d;
--primary-400: #e44a3f;
--primary-500: #da241a;
--primary-600: #c91f17;
--primary-700: #b51b14;
--primary-800: #941711;
--primary-900: #73120d;
--primary-950: #4a0c08;
--background: oklch(97.02% 0 29.2);
--border: oklch(90% 0 29.2);
--danger: #b51b14;
--danger-hover: #941711;
--danger-foreground: oklch(99.11% 0 0);
/* HeroUI control aliases: primary = L1, secondary = L2. */
--default: var(--surface-secondary);
--default-foreground: var(--surface-secondary-foreground);
--field-background: var(--surface);
--default: oklch(94% 0 29.2);
--default-foreground: oklch(21.03% 0.0059 29.2);
--field-background: oklch(100% 0 29.2);
--field-border: transparent;
--field-foreground: var(--surface-foreground);
--field-placeholder: oklch(55.17% 0.009 27.03);
--focus: oklch(45.32% 0.1661 27.03);
--foreground: oklch(21.03% 0.0045 27.03);
--muted: oklch(55.17% 0.009 27.03);
--overlay: var(--surface);
--overlay-foreground: var(--surface-foreground);
--scrollbar: oklch(87.1% 0.0045 27.03);
--segment: oklch(100% 0.0045 27.03);
--segment-foreground: oklch(21.03% 0.0045 27.03);
--separator: oklch(92% 0.0045 27.03);
--success: oklch(73.29% 0.1952 123.59);
--success-foreground: oklch(21.03% 0.0059 123.59);
--surface: oklch(100% 0.0022 27.03);
--surface-foreground: oklch(21.03% 0.0045 27.03);
--surface-secondary: oklch(95.24% 0.0036 27.03);
--surface-secondary-foreground: oklch(21.03% 0.0045 27.03);
--surface-tertiary: oklch(93.73% 0.0036 27.03);
--surface-tertiary-foreground: oklch(21.03% 0.0045 27.03);
--warning: oklch(78.19% 0.1599 45.11);
--warning-foreground: oklch(21.03% 0.0059 45.11);
--field-foreground: oklch(21.03% 0 29.2);
--field-placeholder: oklch(55.17% 0 29.2);
--focus: #da241a;
--foreground: oklch(21.03% 0 29.2);
--muted: oklch(55.17% 0 29.2);
--overlay: oklch(100% 0 29.2);
--overlay-foreground: oklch(21.03% 0 29.2);
--scrollbar: oklch(87.1% 0 29.2);
--segment: oklch(100% 0 29.2);
--segment-foreground: oklch(21.03% 0 29.2);
--separator: oklch(92% 0 29.2);
--success: oklch(73.29% 0.1935 123.85);
--success-foreground: oklch(21.03% 0.0059 123.85);
--surface: oklch(100% 0 29.2);
--surface-foreground: oklch(21.03% 0 29.2);
--surface-secondary: oklch(95.24% 0 29.2);
--surface-secondary-foreground: oklch(21.03% 0 29.2);
--surface-tertiary: oklch(93.73% 0 29.2);
--surface-tertiary-foreground: oklch(21.03% 0 29.2);
--warning: oklch(78.19% 0.1585 45.37);
--warning-foreground: oklch(21.03% 0.0059 45.37);
/* Border Radius */
--radius: 0.5rem;
......@@ -53,6 +71,55 @@
/* Make sure to load Inter font in your app */
--font-sans: var(--font-inter);
/*
* Xinyuan fixed split-shell contract.
* The application canvas always uses this light theme. The Sidebar is the
* only persistent dark plane and must consume these semantic tokens.
*/
--xinyuan-sidebar-background: oklch(16% 0.02 255);
--xinyuan-sidebar-border: oklch(26% 0.022 255);
--xinyuan-sidebar-foreground: oklch(97% 0.004 255);
--xinyuan-sidebar-muted: oklch(76% 0.018 255);
--xinyuan-sidebar-item-hover: oklch(22% 0.026 255);
--xinyuan-sidebar-item-selected: oklch(28% 0.016 255);
--xinyuan-sidebar-item-selected-foreground: oklch(99% 0.002 255);
--xinyuan-sidebar-focus: oklch(78% 0.105 29.2);
/*
* ECharts is the required chart renderer for Xinyuan applications.
* Ant Design remains the icon source. Resolve these CSS variables to
* concrete colors before assigning them to an ECharts option object.
*/
/* Coordinated financial palette: brand reds -> warm neutrals -> gray blues. */
--xinyuan-chart-1: #da241a;
--xinyuan-chart-2: #b83a32;
--xinyuan-chart-3: #c95a52;
--xinyuan-chart-4: #d47a70;
--xinyuan-chart-5: #a65a45;
--xinyuan-chart-6: #b87958;
--xinyuan-chart-7: #c99772;
--xinyuan-chart-8: #6f6674;
--xinyuan-chart-9: #807783;
--xinyuan-chart-10: #938b96;
--xinyuan-chart-11: #4f6380;
--xinyuan-chart-12: #61758f;
--xinyuan-chart-13: #74879a;
--xinyuan-chart-14: #8a6f63;
--xinyuan-chart-15: #9a8174;
--xinyuan-chart-16: #b0a5ad;
--xinyuan-chart-sector-gap: var(--surface);
--xinyuan-chart-sector-radius: 10px;
--xinyuan-chart-grid: #e8e8e8;
--xinyuan-chart-axis: #737373;
--xinyuan-chart-label: #737373;
--xinyuan-chart-tooltip-background: #fff;
--xinyuan-chart-tooltip-border: #dedede;
--xinyuan-chart-tooltip-foreground: #222;
--xinyuan-chart-tooltip-shadow: 0 16px 40px rgb(15 23 42 / 14%);
--xinyuan-chart-legend-font-size: 0.75rem;
--xinyuan-chart-legend-line-height: 1rem;
}
.dark,
......@@ -60,37 +127,53 @@
color-scheme: dark;
/* Theme Colors (Dark Mode) */
--accent: oklch(45.32% 0.1661 27.03);
--accent: #da241a;
--accent-hover: #c91f17;
--accent-pressed: #b51b14;
--accent-foreground: oklch(99.11% 0 0);
--background: oklch(12% 0.0045 27.03);
--border: oklch(28% 0.0045 27.03);
--danger: oklch(62% 0.16 27.03);
--primary: #da241a;
--primary-hover: #c91f17;
--primary-pressed: #b51b14;
--primary-foreground: #fff;
--primary-50: #fce9e7;
--primary-100: #f9d0cc;
--primary-200: #f3a29a;
--primary-300: #ec756d;
--primary-400: #e44a3f;
--primary-500: #da241a;
--primary-600: #c91f17;
--primary-700: #b51b14;
--primary-800: #941711;
--primary-900: #73120d;
--primary-950: #4a0c08;
--background: oklch(12% 0 29.2);
--border: oklch(28% 0 29.2);
--danger: #b51b14;
--danger-hover: #941711;
--danger-foreground: oklch(99.11% 0 0);
/* HeroUI control aliases: primary = L1, secondary = L2. */
--default: var(--surface-secondary);
--default-foreground: var(--surface-secondary-foreground);
--field-background: var(--surface);
--default: oklch(27.4% 0 29.2);
--default-foreground: oklch(99.11% 0 0);
--field-background: oklch(21.03% 0 29.2);
--field-border: transparent;
--field-foreground: var(--surface-foreground);
--field-placeholder: oklch(70.5% 0.009 27.03);
--focus: oklch(45.32% 0.1661 27.03);
--foreground: oklch(99.11% 0.0045 27.03);
--muted: oklch(70.5% 0.009 27.03);
--overlay: var(--surface);
--overlay-foreground: var(--surface-foreground);
--scrollbar: oklch(70.5% 0.0045 27.03);
--segment: oklch(39.64% 0.0045 27.03);
--segment-foreground: oklch(99.11% 0.0045 27.03);
--separator: oklch(25% 0.0045 27.03);
--success: oklch(73.29% 0.1952 123.59);
--success-foreground: oklch(21.03% 0.0059 123.59);
--surface: oklch(21.03% 0.009 27.03);
--surface-foreground: oklch(99.11% 0.0045 27.03);
--surface-secondary: oklch(25.7% 0.0067 27.03);
--surface-secondary-foreground: oklch(99.11% 0.0045 27.03);
--surface-tertiary: oklch(27.21% 0.0067 27.03);
--surface-tertiary-foreground: oklch(99.11% 0.0045 27.03);
--warning: oklch(82.03% 0.14 49.12);
--warning-foreground: oklch(21.03% 0.0059 49.12);
--field-foreground: oklch(99.11% 0 29.2);
--field-placeholder: oklch(70.5% 0 29.2);
--focus: #da241a;
--foreground: oklch(99.11% 0 29.2);
--muted: oklch(70.5% 0 29.2);
--overlay: oklch(21.03% 0 29.2);
--overlay-foreground: oklch(99.11% 0 29.2);
--scrollbar: oklch(70.5% 0 29.2);
--segment: oklch(39.64% 0 29.2);
--segment-foreground: oklch(99.11% 0 29.2);
--separator: oklch(25% 0 29.2);
--success: oklch(73.29% 0.1935 123.85);
--success-foreground: oklch(21.03% 0.0059 123.85);
--surface: oklch(21.03% 0 29.2);
--surface-foreground: oklch(99.11% 0 29.2);
--surface-secondary: oklch(25.7% 0 29.2);
--surface-secondary-foreground: oklch(99.11% 0 29.2);
--surface-tertiary: oklch(27.21% 0 29.2);
--surface-tertiary-foreground: oklch(99.11% 0 29.2);
--warning: oklch(82.03% 0.1388 49.38);
--warning-foreground: oklch(21.03% 0.0059 49.38);
}
......@@ -2,9 +2,7 @@
.light,
.default,
[data-theme='light'],
[data-theme='default'],
.dark,
[data-theme='dark'] {
[data-theme='default'] {
/* Xinyuan aliases and extension tokens. HeroUI core tokens are owned by theme.css. */
--rc-brand-primary: var(--accent);
--rc-brand-light: color-mix(in oklab, var(--accent) 84%, white);
......@@ -26,6 +24,19 @@
var(--background)
);
--rc-text-muted: var(--muted);
--rc-text-empty: color-mix(in oklab, var(--muted) 72%, var(--background));
/* Fixed dark Sidebar aliases. The content canvas remains light. */
--rc-sidebar-background: var(--xinyuan-sidebar-background);
--rc-sidebar-border: var(--xinyuan-sidebar-border);
--rc-sidebar-foreground: var(--xinyuan-sidebar-foreground);
--rc-sidebar-muted: var(--xinyuan-sidebar-muted);
--rc-sidebar-item-hover: var(--xinyuan-sidebar-item-hover);
--rc-sidebar-item-selected: var(--xinyuan-sidebar-item-selected);
--rc-sidebar-item-selected-foreground: var(
--xinyuan-sidebar-item-selected-foreground
);
--rc-sidebar-focus: var(--xinyuan-sidebar-focus);
--rc-font-sans: var(--font-sans);
--rc-font-medium: 500;
--rc-font-semibold: 600;
......@@ -42,6 +53,7 @@
/* Page geometry only. Component appearance remains owned by HeroUI. */
--rc-shell-inline-gutter: 3rem;
--rc-sidebar-width: 15rem;
--rc-navbar-inline-padding: var(--rc-shell-inline-gutter);
--rc-page-inline-start: var(--rc-shell-inline-gutter);
--rc-page-inline-end: var(--rc-shell-inline-gutter);
......@@ -70,31 +82,20 @@
--rc-login-brand-size: 1.5rem;
--rc-login-brand-weight: var(--rc-font-semibold);
--rc-login-brand-gap: 1.5rem;
/* Shared modal width ladder from the Xinyuan overlay contract. */
--rc-dialog-width-xs: 20rem;
--rc-dialog-width-sm: 24rem;
--rc-dialog-width-md: 30rem;
--rc-dialog-width-lg: 40rem;
--rc-dialog-width-xl: 46rem;
--rc-dialog-width-2xl: 52.5rem;
--rc-dialog-width-3xl: 60rem;
--rc-dialog-width-4xl: 68rem;
--rc-dialog-width-5xl: 76rem;
--rc-shadow-raised: 0 1px 2px rgb(15 23 42 / 6%),
0 8px 24px rgb(15 23 42 / 8%);
--rc-shadow-overlay: 0 16px 40px rgb(15 23 42 / 14%);
--rc-shadow-card: 0 1px 2px rgb(15 23 42 / 2%), 0 4px 12px rgb(15 23 42 / 2%);
/* KPI icon surfaces. Card bodies remain neutral; only the icon well carries tone. */
--rc-metric-tone-info: oklch(55% 0.14 255);
--rc-metric-tone-success: oklch(54% 0.13 145);
--rc-metric-tone-teal: oklch(55% 0.1 205);
--rc-metric-tone-danger: oklch(52% 0.17 25);
--rc-metric-tone-warning: oklch(62% 0.12 75);
--rc-metric-tone-violet: oklch(56% 0.12 300);
--rc-metric-tone-indigo: oklch(53% 0.13 275);
--rc-metric-tone-coral: oklch(60% 0.12 48);
--rc-metric-tone-info: var(--rc-chart-11);
--rc-metric-tone-success: var(--rc-chart-12);
--rc-metric-tone-teal: var(--rc-chart-13);
--rc-metric-tone-danger: var(--rc-chart-1);
--rc-metric-tone-warning: var(--rc-chart-6);
--rc-metric-tone-violet: var(--rc-chart-8);
--rc-metric-tone-indigo: var(--rc-chart-9);
--rc-metric-tone-coral: var(--rc-chart-3);
--rc-metric-tone-neutral: var(--muted);
--rc-metric-tone-info-soft: color-mix(
in oklab,
......@@ -143,39 +144,28 @@
);
/* Data visualization only. Do not reuse UI status colors as a series palette. */
--rc-chart-1: oklch(45.32% 0.1661 27.03);
--rc-chart-2: oklch(56% 0.135 255);
--rc-chart-3: oklch(64% 0.11 220);
--rc-chart-4: oklch(56% 0.115 280);
--rc-chart-5: oklch(60% 0.095 200);
--rc-chart-6: oklch(70% 0.12 80);
--rc-chart-7: oklch(60% 0.1 295);
--rc-chart-8: oklch(57% 0.035 250);
--rc-chart-1: var(--xinyuan-chart-1);
--rc-chart-2: var(--xinyuan-chart-2);
--rc-chart-3: var(--xinyuan-chart-3);
--rc-chart-4: var(--xinyuan-chart-4);
--rc-chart-5: var(--xinyuan-chart-5);
--rc-chart-6: var(--xinyuan-chart-6);
--rc-chart-7: var(--xinyuan-chart-7);
--rc-chart-8: var(--xinyuan-chart-8);
--rc-chart-9: var(--xinyuan-chart-9);
--rc-chart-10: var(--xinyuan-chart-10);
--rc-chart-11: var(--xinyuan-chart-11);
--rc-chart-12: var(--xinyuan-chart-12);
--rc-chart-13: var(--xinyuan-chart-13);
--rc-chart-14: var(--xinyuan-chart-14);
--rc-chart-15: var(--xinyuan-chart-15);
--rc-chart-16: var(--xinyuan-chart-16);
--rc-chart-primary: var(--rc-chart-1);
--rc-chart-risk: oklch(47% 0.145 27.03);
--rc-chart-warning: var(--rc-chart-6);
--rc-chart-positive: var(--rc-chart-2);
--rc-chart-comparison: var(--rc-chart-4);
--rc-chart-risk: var(--rc-chart-1);
--rc-chart-warning: var(--rc-chart-3);
--rc-chart-positive: var(--rc-chart-7);
--rc-chart-comparison: var(--rc-chart-5);
--rc-chart-neutral: var(--rc-chart-8);
--rc-chart-legend-font-size: 0.75rem;
--rc-chart-legend-line-height: 1rem;
}
.dark,
[data-theme='dark'] {
color-scheme: dark;
--rc-text-secondary: color-mix(
in oklab,
var(--foreground) 76%,
var(--background)
);
--rc-button-secondary-border: color-mix(
in oklab,
var(--foreground) 18%,
transparent
);
--rc-shadow-raised: 0 1px 2px rgb(0 0 0 / 24%), 0 8px 24px rgb(0 0 0 / 24%);
--rc-shadow-overlay: 0 16px 40px rgb(0 0 0 / 38%);
--rc-shadow-card: 0 1px 2px rgb(0 0 0 / 20%), 0 4px 12px rgb(0 0 0 / 18%);
--rc-chart-legend-font-size: var(--xinyuan-chart-legend-font-size);
--rc-chart-legend-line-height: var(--xinyuan-chart-legend-line-height);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment