From 34e9d118ea1782f92658a416477e27e43bf907fb Mon Sep 17 00:00:00 2001 From: bex Date: Mon, 2 Nov 2026 13:59:04 +0800 Subject: [PATCH 1/6] fix: keep sidebar collapse control at top --- AGENTS.md | 2 +- .../biu-foundation-components-capabilities.md | 4 ++-- docs/design.md | 2 +- .../biu-foundation-components-capabilities.md | 4 ++-- docs/en/design.md | 2 +- docs/en/final-delivery-review.md | 4 ++-- docs/final-delivery-review.md | 4 ++-- docs/ui-comparison.md | 2 +- docs/usage.md | 2 +- packages/preset/README.md | 2 +- packages/preset/src/layout-frame.tsx | 5 ++--- packages/preset/src/layout-sidebar-footer.tsx | 21 +++++++------------ packages/preset/src/styles/sidebar.css | 14 ++++++------- .../preset/test/sidebar-controls.test.mjs | 14 ++++++------- 14 files changed, 36 insertions(+), 46 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bf5b51b..5adb9e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,7 +73,7 @@ apps// ## 最终交付归档 - 首版发布前的回归结果、评分、已完成项、TODO、GitHub Secrets 和生产接入边界统一记录在 `docs/final-delivery-review.md` 及 `docs/en/final-delivery-review.md`。 -- HTML、Vue、React 独立 APP 必须共用 `@biugle/biu-preset` 的 `LayoutFrame`;收起状态统一为 60px 图标栏和底部单一控制按钮,顶部恢复按钮只用于“隐藏菜单栏”,禁止 Adapter 或 Demo 自行复制收起逻辑。 +- HTML、Vue、React 独立 APP 必须共用 `@biugle/biu-preset` 的 `LayoutFrame`;收起状态统一为 60px 图标栏,收起/展开按钮始终固定在侧栏顶部,展开态位于侧栏与内容区交界处,收起态居中在图标栏内并只显示图标;底部始终保留菜单配置入口。菜单完全隐藏时,顶部按钮仍在 `aside` 外,避免 `aria-hidden/inert` 阻断恢复;禁止 Adapter 或 Demo 自行复制收起逻辑。 - 交付门禁统一执行 `pnpm verify`:公共包/Demo 类型检查、18 个公开包打包目标与污染检查、公共包回归单测、ESLint、Prettier、Knip、生产依赖审计、100 个 CLI 创建输入、7 种 Portal/APP preset 与环境构建、全量 Demo 构建和 `git diff --check`;正式 npm/Vercel 发布仍需配置仓库 Secrets 后由 GitHub Actions 执行。 - 首次提交前必须检查 staged diff,确保无 Token、Cookie、密码、内部域名或本地用户数据;不要提交 `dist/`、`.biu/`、`node_modules/`、缓存和 `.husky/_/`。 - 组件、基座、公共包、Demo、API、设计决策、浏览器回归和交付约束的唯一综合记录是 `docs/biu-foundation-components-capabilities.md` 及其 `docs/en/` 英文对应文件;`docs/final-delivery-review.md` 只保存发布审计,不得再建立或保留 `docs/specs/` 阶段性 Spec 目录。 diff --git a/docs/biu-foundation-components-capabilities.md b/docs/biu-foundation-components-capabilities.md index 4488064..23b6b4c 100644 --- a/docs/biu-foundation-components-capabilities.md +++ b/docs/biu-foundation-components-capabilities.md @@ -261,7 +261,7 @@ Button 在鼠标或键盘激活后必须保留 focus,并与 hover、active、f - 基座不再维护独立 Tooltip、Ellipsis、Message、Modal、Drawer、Select、Table、Form 行为;只保留布局骨架、尺寸约束和适配器边界。 - 基座骨架、原有菜单交互、收起/展开逻辑、Tabs、响应式和页面布局不能因组件抽离而改变。Portal 与 APP 同级、独立启动、独立构建、独立部署;默认 iframe。 - 独立 APP 的布局壳由 CLI 按开发/构建模式生成:`layout.includeInDevelopment` 与 `layout.includeInBuild` 分开控制 Preset/Custom 和 CSS 导入;新标准 APP 模板默认开发有模拟壳、生产无壳,显式选择 `custom` 的 APP 默认保留自有布局,`biu init` 可逐项目选择。旧配置未填写时延续原来的双模式布局输出,Portal 始终保留布局;关闭 APP 布局不会移除 Runtime、路由、认证出口或错误边界。 -- Sidebar 默认完整展开;展开状态的 28px 圆形控制按钮(14px 图标)在 60px Header 内居中,按 LTR/RTL 锚定侧栏与内容分界,用于收起为 60px 图标栏。收起状态仅在底部保留一个恢复按钮;完全隐藏时则在 `aside` 外保留顶部恢复按钮,避免 `aria-hidden/inert` 阻断恢复,Header 为按钮和相邻操作保留 8px 间隙。展开侧栏底部依次提供隐藏、菜单选项、收藏、最近使用和按配置启用的搜索;`lockSidebar` 禁用全部内置切换,页面的受控覆盖继续有效。面包屑左右内边距各 12px,Runtime 文档根保持 LTR,基座按显式配置应用方向。 +- Sidebar 默认完整展开;28px 圆形控制按钮(14px 图标)在 60px Header 内垂直居中,按 LTR/RTL 锚定位置:展开态位于侧栏与内容区交界处,收起为 60px 图标栏后居中在栏内并只显示展开图标。底部始终保留菜单配置入口,展开状态另外提供隐藏、收藏、最近使用和按配置启用的搜索。完全隐藏时顶部按钮仍在 `aside` 外,避免 `aria-hidden/inert` 阻断恢复,Header 为按钮和相邻操作保留 8px 间隙。`lockSidebar` 禁用全部内置切换,页面的受控覆盖继续有效。面包屑左右内边距各 12px,Runtime 文档根保持 LTR,基座按显式配置应用方向。 ### 7.1 非菜单页面的组件复用边界 @@ -408,7 +408,7 @@ Select、Cascader、Transfer 等选择组件不得静默丢弃调用方或后端 - `LocalizedDemoTree` 只递归翻译普通对象的文案属性;Dayjs、Date、File 等实例保持原型与身份。日期范围默认值不能被复制为普通对象,否则 DatePicker 的 `startOf` 等方法失效。真实浏览器已复现该错误,旧实现负例和修复后日期范围/嵌套选项回归保留在 Demo 测试中。 - Form Demo 的独立控件通过 `ConfigProvider locale={locale}` 继承当前语言;ProForm 的表单文案语言不替代控件 Provider。组件章节、案例和 API 可见文案补齐英文资源,分页/查看/移除/动态序号用完整中文插值 key。菜单和已打开页签由当前资源即时重译,身份、地理 mock、日期、Code 和类型保持示例值。 -- 收起的 60px 导航栏只保留底部恢复按钮,展开状态保留原有设置/隐藏操作;菜单完全隐藏时在导航外保留顶部恢复按钮,避免 `inert` 阻止恢复。Portal 与 React/Vue/HTML APP 共用 LayoutFrame 和同一状态变更入口,RTL 同步;公开布局参数不变。 +- 收起的 60px 导航栏在顶部保留图标式展开按钮,底部始终保留菜单配置;展开状态底部另有隐藏、收藏、最近使用和搜索操作。菜单完全隐藏时顶部按钮仍在导航外,避免 `inert` 阻止恢复。Portal 与 React/Vue/HTML APP 共用 LayoutFrame 和同一状态变更入口,RTL 同步;公开布局参数不变。 - Vue/HTML Demo 各自通过 `localeUrl` 加载独立中英文资源。Vue 页面用项目级组合函数订阅公共 I18n;HTML 项目 Adapter 复用公共 `htmlAdapter`,只翻译显式 `data-biu-locale` 叶节点。语言与资源刷新都会更新正文,卸载时清理订阅;不向 Runtime 添加 Demo 文案或第二套布局。 - CLI 为 HTML 项目优先使用显式 `adapter`,未配置时继续使用内置 `htmlAdapter`;开发与生产入口均覆盖回归。Runtime 的文档标题在异步资源加载和同语言重载后同步重新翻译,不追加语言请求。 - 通知的 `title/description/time` 是通知接口返回的业务数据,基座原样展示;`src/mock/notifications.ts` 的两条中文通知是此数据契约的脱敏样例,属于明确文案例外。生产通知服务应按目标语言返回正文。英文长标题用例用于验证省略与多层导航边界,保留示例字符串;它们不进入语言资源作为英文 key。 diff --git a/docs/design.md b/docs/design.md index 8108158..a248296 100644 --- a/docs/design.md +++ b/docs/design.md @@ -126,7 +126,7 @@ export default [ | `mobile` | 移动端底部导航 | | `custom` | React 独立自定义模式,仅保留 Runtime 能力 | -`tabs` 与 `breadcrumb` 是独立配置项,默认可以关闭。Sidebar 目录展开由 Menu Store 管理;展开状态的顶部边界按钮收起到 60px 图标栏,收起栏底部只保留单一恢复按钮。菜单完全隐藏时在导航外提供顶部恢复按钮。展开状态底部提供隐藏菜单栏、菜单选项、收藏、最近使用和可选搜索,恢复回完整侧栏后所有操作仍可用。菜单配置面板提供全部展开、全部收起、展开/折叠菜单栏、隐藏菜单栏、单目录展开、多目录展开、顶部展示搜索按钮、目录标题显示和单栏/双栏模式,除“隐藏菜单栏”外操作不会关闭面板;顶部展示搜索按钮开启后复用菜单栏搜索组件,将搜索按钮放到通知按钮左侧并隐藏底部重复入口;显示/隐藏目录标题只在双栏模式生效。收藏按门户 scope 使用 Menu Store 和 localStorage 隔离,最多 100 条;最近使用最多 10 条,支持一次还原全部标签,旧记录保留并交给页面不存在兜底。双栏模式直接使用后端 Tree 的第一级目录作为左侧图标栏,右侧显示当前目录标题和菜单;第一级目录下允许目录和菜单同级,目录可以继续嵌套目录和菜单,右侧层级统一不显示图标;目录 Code 在不同分支下可以重复,实际页面仍以菜单节点自身的稳定路径 Key 为准。左侧目录栏高度受侧栏内容区约束,超出时显示上下滚动按钮;没有配置时保持单栏 Sidebar。普通菜单项默认无图标,只有目录显示配置图标,没有配置使用默认目录图标,不通过 Code 或标题猜图标。 +`tabs` 与 `breadcrumb` 是独立配置项,默认可以关闭。Sidebar 目录展开由 Menu Store 管理;侧栏顶部边界按钮始终负责收起/展开,展开态位于侧栏与内容区交界处,收起到 60px 图标栏时居中在栏内并只显示展开图标。菜单完全隐藏时,顶部恢复按钮仍在导航外,避免 `inert` 阻止恢复。底部始终保留菜单配置入口;展开状态还提供隐藏菜单栏、收藏、最近使用和可选搜索。菜单配置面板提供全部展开、全部收起、展开/折叠菜单栏、隐藏菜单栏、单目录展开、多目录展开、顶部展示搜索按钮、目录标题显示和单栏/双栏模式,除“隐藏菜单栏”外操作不会关闭面板;顶部展示搜索按钮开启后复用菜单栏搜索组件,将搜索按钮放到通知按钮左侧并隐藏底部重复入口;显示/隐藏目录标题只在双栏模式生效。收藏按门户 scope 使用 Menu Store 和 localStorage 隔离,最多 100 条;最近使用最多 10 条,支持一次还原全部标签,旧记录保留并交给页面不存在兜底。双栏模式直接使用后端 Tree 的第一级目录作为左侧图标栏,右侧显示当前目录标题和菜单;第一级目录下允许目录和菜单同级,目录可以继续嵌套目录和菜单,右侧层级统一不显示图标;目录 Code 在不同分支下可以重复,实际页面仍以菜单节点自身的稳定路径 Key 为准。左侧目录栏高度受侧栏内容区约束,超出时显示上下滚动按钮;没有配置时保持单栏 Sidebar。普通菜单项默认无图标,只有目录显示配置图标,没有配置使用默认目录图标,不通过 Code 或标题猜图标。 菜单项 hover 时提供“新标签页打开”箭头,点击使用浏览器原生新标签页。展开侧栏时普通菜单不显示图标,收起侧栏时显示统一的页面占位图标;目录图标仍只来自显式配置或默认目录图标。菜单、Tabs、用户名称都限制最大宽度,超长省略并通过 Tooltip 展示完整文本。Breadcrumb 保留完整多级路径,Tabs 只展示最后一级页面名,完整链路仅作为 Tooltip。工具区、Tabs 区和导航区拥有独立溢出滚动;小屏保证至少展示一个 Tab/工具图标,门户切换空间不足时保留文本入口且不渲染冗余下拉箭头。Portal 可通过 `portalSlots.workbar` 和 `portalSlots.toolbar` 插入工作栏、部门/角色切换、自定义搜索和按钮,也可以完全替换默认工具栏。 diff --git a/docs/en/biu-foundation-components-capabilities.md b/docs/en/biu-foundation-components-capabilities.md index 4d554ce..ba6549d 100644 --- a/docs/en/biu-foundation-components-capabilities.md +++ b/docs/en/biu-foundation-components-capabilities.md @@ -121,7 +121,7 @@ Topbar directory and nested popovers give rows and their Popover/Tooltip wrapper The CLI generates standalone APP layout shells by mode. `layout.includeInDevelopment` and `layout.includeInBuild` independently control Preset/Custom and CSS imports. New standard APP templates default to a development preview shell and a shell-free production bundle; an APP created with the explicit `custom` preset keeps its user-owned layout by default. `biu init` can choose each APP's production policy. Existing configs that omit these flags preserve their former output, and Portals always keep their layouts. Disabling an APP layout does not remove Runtime, routing, authentication exits or error boundaries. -The Sidebar preset starts expanded. Its 28px round control (14px icon) is centered within the 60px Header at the sidebar/content seam and follows the explicit LTR/RTL direction. It collapses navigation into a 60px icon rail. That rail has one footer restore action. Fully hidden navigation instead keeps a top recovery control outside the inert `aside`, with an 8px gap before adjacent Header actions. Expanded footer actions provide hide, settings, favorites, recent pages and optional search. `lockSidebar` disables built-in presentation changes; controlled host overrides remain supported. Breadcrumb padding is 12px per side. Runtime keeps the document root LTR and applies configured direction at the shell boundary. +The Sidebar preset starts expanded. Its 28px round control (14px icon) is vertically centered in the 60px Header and follows the explicit LTR/RTL direction: it sits at the sidebar/content seam while expanded, then centers inside the 60px icon rail and shows only the expand icon when collapsed. Menu settings remain in the footer in both states; expanded navigation additionally shows hide, favorites, recent pages and optional search. Fully hidden navigation keeps the same top control outside the inert `aside`, with an 8px gap before adjacent Header actions. `lockSidebar` disables built-in presentation changes; controlled host overrides remain supported. Breadcrumb padding is 12px per side. Runtime keeps the document root LTR and applies configured direction at the shell boundary. ### Non-menu foundation pages @@ -228,7 +228,7 @@ After restarting port 9002, the following capability surfaces were checked in th - `LocalizedDemoTree` translates text fields only in plain records. Dayjs, Date and File instances retain their prototype and identity. Date ranges must not become plain objects, which would remove DatePicker methods such as `startOf`. The failure was reproduced in the browser; Demo regressions cover the old failure and fixed date ranges/nested option labels. - Form Demo controls inherit the selected language from `ConfigProvider locale={locale}`; ProForm text localization does not replace a control Provider. Component sections, examples and visible API text have independent English resources. Pagination/view/remove/item-number callbacks use complete Chinese interpolation keys. Menus and open tabs translate against current resources; identities, geographic mock values, dates, Codes and types remain example data. -- A collapsed 60px navigation rail has one footer restore button. Expanded navigation keeps its settings/hide actions; fully hidden navigation retains a top restore button outside the inert sidebar. Portal and React/Vue/HTML APP share LayoutFrame and one state-update path, including RTL. Public layout parameters remain unchanged. +- A collapsed 60px navigation rail keeps its icon-only expand button at the top edge and menu settings in the footer. Expanded navigation additionally keeps hide/favorites/recent/search actions; fully hidden navigation retains the top control outside the inert sidebar. Portal and React/Vue/HTML APP share LayoutFrame and one state-update path, including RTL. Public layout parameters remain unchanged. - Vue/HTML Demos each load independent bilingual resources through `localeUrl`. Vue uses a project composition function to subscribe to public I18n; the HTML project Adapter reuses public `htmlAdapter` and translates only explicit `data-biu-locale` leaf nodes. Locale/resource changes update page content and unmount removes subscriptions. Runtime receives no Demo text or duplicate layout. - CLI honors explicit HTML `adapter` configuration and retains built-in `htmlAdapter` when omitted; regressions cover both development and production entries. Runtime retranslates document titles after asynchronous resource loading and same-language reload without issuing extra locale requests. - Notification `title/description/time` are business data returned by the notification API and render verbatim. The two Chinese records in `src/mock/notifications.ts` are sanitized examples of that contract and an explicit localization exception; a production service should return content for the target language. English long-title navigation examples retain their strings to test truncation/hierarchy and do not add English translation keys. diff --git a/docs/en/design.md b/docs/en/design.md index 2557f20..c97fdd6 100644 --- a/docs/en/design.md +++ b/docs/en/design.md @@ -67,4 +67,4 @@ Bridge host context contains locale, theme, timezone, direction, environment, po The root workspace and generated projects include TypeScript, ESLint, Prettier, EditorConfig and Husky/lint-staged. Knip audits public packages; Changesets manages versions and GitHub Actions runs the quality gates. Demo apps are private acceptance projects, not published packages. -Sidebar presentation uses one shared LayoutFrame: expanded navigation has its edge collapse control and normal footer actions; the 60px icon rail has a single footer restore control. Fully hidden navigation restores through the top control outside the inert sidebar. Public controlled overrides and locking remain supported. +Sidebar presentation uses one shared LayoutFrame: the collapse/expand control stays at the top, at the sidebar/content seam when expanded and centered inside the 60px icon rail when collapsed. It shows only the expand icon in the collapsed rail. Menu settings remain in the footer in both states; expanded navigation additionally shows hide, favorites, recent pages and optional search. Fully hidden navigation restores through the same top control outside the inert sidebar. Public controlled overrides and locking remain supported. diff --git a/docs/en/final-delivery-review.md b/docs/en/final-delivery-review.md index 78b52ba..163db12 100644 --- a/docs/en/final-delivery-review.md +++ b/docs/en/final-delivery-review.md @@ -25,7 +25,7 @@ Production old-hash caching, real SSO/CSP/monitoring and a 30-minute browser hea No whole script was found removable without losing an active caller. Dead branches and redundant records were removed. Every JS/JSX/MJS/TS/TSX/CSS file has an 800-line limit without exceptions. Locale JSON and lockfiles are data; their complete structures remain. Additional translation resources fill missing visible text, and additional source files reflect separate responsibilities. -Browser follow-up: Demo localization previously copied Dayjs range instances into plain records, crashing DatePicker. Instances now retain prototypes/identity and two regressions cover the boundary. Shared LayoutFrame uses one footer restore action in the collapsed 60px rail and keeps top recovery outside fully hidden navigation. Expansion/hiding/recovery and public controlled layout parameters remain. Notification content, actual mock values and English long-title boundary examples are documented data/display exceptions. +Browser follow-up: Demo localization previously copied Dayjs range instances into plain records, crashing DatePicker. Instances now retain prototypes/identity and two regressions cover the boundary. Shared LayoutFrame keeps an icon-only expand control at the top edge of the collapsed 60px rail and menu settings in the footer in every state; fully hidden navigation restores through the same top control outside the navigation. Expansion/hiding/recovery and public controlled layout parameters remain. Notification content, actual mock values and English long-title boundary examples are documented data/display exceptions. This release review additionally fixed locale changes resetting update baselines and retired requests displaying notices after unmount, disable or manifest URL replacement. ArrayBuffer, DataView and TypedArray uploads use client-scoped object identity without canceling distinct instances or traversing large buffers; identical instances still deduplicate and public serialization stays compatible. Release uses `queue: max` to retain up to 100 pending runs so later ordinary pushes cannot replace queued Release PR merge runs. Actual publication on the target GitHub Actions platform still needs operational acceptance. @@ -92,7 +92,7 @@ Previous browser evidence for the version with 477 passing tests: | Sidebar Portal / standard Table | 110 records, 20 per page; query returns 19 and reset restores 110; column hide/show works; width 180→212→reset180px; no outer desktop overflow | | Narrow standard Table | Fresh innerWidth/pageWidth measurements both equal 390px; advanced Drawer and inline fields share state, action column is 60px, no outer overflow | | Topbar Portal | Two nested levels remain readable, directory button width 208px, full Overview URL works | -| React / Vue / HTML APP | Shared LayoutFrame collapses to a 60px rail with one footer restore button; fully hidden React navigation recovers from its top control | +| React / Vue / HTML APP | Shared LayoutFrame keeps the collapse/expand control at the top edge and menu settings in the footer in expanded and collapsed states; hidden navigation restores from the top control | | Vue / HTML locale | Content, menus and browser titles switch to English; Vue counter advances 0 to 1; explicit HTML project Adapter is used | | ComponentUI / Form / Table capabilities | Fresh English API/example refresh, September 20 selection and Escape close pass; Form required validation and aria-invalid pass; no console errors/warnings | | React Custom | fire.render body content and Dialog mount/close correctly; browser console has no errors/warnings | diff --git a/docs/final-delivery-review.md b/docs/final-delivery-review.md index c3e96db..b80666c 100644 --- a/docs/final-delivery-review.md +++ b/docs/final-delivery-review.md @@ -25,7 +25,7 @@ 未发现可无损删除的整份无调用方脚本;实际无用分支与重复记录已删除。所有 JS/JSX/MJS/TS/TSX/CSS 统一不超过 800 行,无豁免。JSON 语言资源和锁文件属于数据,保留完整结构而不为凑行数拆分。中英文资源增加是补齐原先缺失的可见文案,代码拆分后的文件数增加对应清晰职责。 -浏览器补充排错:Demo 翻译递归曾将 Dayjs 日期范围实例复制为普通对象,导致 DatePicker 崩溃;现保留实例原型/身份,并加入两项回归。共享 LayoutFrame 的 60px 收起栏统一为底部单一恢复按钮,完全隐藏时仍使用导航外的顶部恢复;展开/隐藏/恢复与公开受控布局参数保留。通知正文、真实 mock 值和英文长标题边界案例属于已明确说明的数据/展示例外。 +浏览器补充排错:Demo 翻译递归曾将 Dayjs 日期范围实例复制为普通对象,导致 DatePicker 崩溃;现保留实例原型/身份,并加入两项回归。共享 LayoutFrame 的 60px 收起栏在顶部边界保留图标式展开按钮,底部始终保留菜单配置;完全隐藏时仍从导航外的顶部按钮恢复。展开/隐藏/恢复与公开受控布局参数保留。通知正文、真实 mock 值和英文长标题边界案例属于已明确说明的数据/展示例外。 本次发版复核新增修复:语言切换不再重置更新清单基线,提示使用当前语言;卸载、禁用或更换清单 URL 后的迟到响应不再提示。ArrayBuffer、DataView 和 TypedArray 上传使用 client 内对象身份,不误取消不同实例,也不遍历大数组字节;相同实例仍去重,公开序列化输出不变。Release 使用 `queue: max` 保留最多 100 个 pending,后续普通 push 不再替换排队的 Release PR 合并运行;目标 GitHub Actions 的实际发布运行仍需平台验收。 @@ -92,7 +92,7 @@ main-a 中英文资源各 3411 条,英文资源从本轮基线 2742 条增至 | Sidebar Portal 与标准 Table | 默认 110 条、每页 20;查询 19 条、重置 110;字段隐藏/恢复;列宽 180→212→重置180px;桌面无外层横向溢出 | | 窄屏标准 Table | 当时实测 innerWidth/pageWidth 均 390px:更多筛选 Drawer 与外层状态同步、操作列 60px、页面无外层横向溢出 | | Topbar Portal | 两级菜单完整可见,子目录按钮宽度 208px,完整层级 Overview URL 正常 | -| React / Vue / HTML APP | 共享 LayoutFrame 的收起栏均 60px、底部单一恢复按钮;React 隐藏后顶部恢复有效 | +| React / Vue / HTML APP | 共享 LayoutFrame 的收起栏均 60px、顶部图标式展开按钮与底部菜单配置入口常驻;React 隐藏后顶部恢复有效 | | Vue / HTML 语言 | 正文、菜单与浏览器标题完整切换英文;Vue 计数从 0 到 1;HTML 显式项目 Adapter 生效 | | ComponentUI / Form / Table 能力 | 当时英文 API/案例刷新正常;日期选 9 月 20 日即时回显、Escape 关闭;Form 必填校验与 aria-invalid 通过,控制台无错误/警告 | | React Custom | fire.render body 内容与 Dialog 挂载、关闭正常,浏览器控制台无错误/警告 | diff --git a/docs/ui-comparison.md b/docs/ui-comparison.md index 40af351..db33cf0 100644 --- a/docs/ui-comparison.md +++ b/docs/ui-comparison.md @@ -4,7 +4,7 @@ ## 1. 结论 -`biu` 已经覆盖基座日常使用的核心交互,并把 UI 维护集中在 `@biugle/biu-preset`,业务项目只配置 preset 和数据。标准 `sidebar` Layout 现在采用左侧全高品牌区、菜单区、底部收起按钮,以及右侧工作区顶部的全局操作区;DOM 层级与企业门户参考页面的“侧栏 + 工作区”结构一致,Portal 与独立 APP 均可独立运行。 +`biu` 已经覆盖基座日常使用的核心交互,并把 UI 维护集中在 `@biugle/biu-preset`,业务项目只配置 preset 和数据。标准 `sidebar` Layout 现在采用左侧全高品牌区、菜单区、顶部边界收起/展开按钮、底部菜单配置入口,以及右侧工作区顶部的全局操作区;DOM 层级与企业门户参考页面的“侧栏 + 工作区”结构一致,Portal 与独立 APP 均可独立运行。 视觉基线采用企业门户常见的紧凑工具栏、当前时间显示、无方框菜单图标、深色激活菜单、轻量 Tabs、页签滚动/刷新/关闭工具组和低对比度内容画布;颜色、文案和上下文通信仍由 biu 自己管理。 diff --git a/docs/usage.md b/docs/usage.md index f4de1dc..7f3db75 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -235,7 +235,7 @@ CLI 构建阶段没有浏览器 Origin,远程菜单与权限接口必须在对 除 Portal 常用的 `sidebar`、`topbar` 外,独立 APP 还可选择 `blank`、`dashboard`、`mobile`;完全自行定义外壳的 React 独立 APP 使用 `custom`。Custom 不渲染官方导航,但仍保留 Runtime Context、错误边界、认证出口和更新检查。 -`layout.preset` 可选 `sidebar`、`topbar`、`blank`、`dashboard`、`mobile`;`tabs` 和 `breadcrumb` 独立配置。Sidebar 底部有菜单选项、收藏、最近使用、搜索、收起/展开;收起侧栏时只显示收起/展开。菜单选项中的“顶部展示搜索按钮”开启后,复用底部搜索弹窗组件,在通知按钮左侧显示搜索按钮,并移除底部重复按钮;“隐藏菜单栏”只关闭菜单栏。收藏和最近使用由菜单 Store 管理,并按门户 Code 与环境隔离写入 `localStorage`,收藏上限 100 条、最近使用上限 10 条。开启 Tabs 后,页签顺序和当前页按同一 scope 写入 `sessionStorage`;语言、主题、时区和方向也只在当前会话内按 Portal/环境隔离。可选双栏模式,目录标题选项仅双栏模式有效。双栏模式的一级目录显示在左侧图标栏,一级目录下的目录和菜单可以同级,目录还可以继续嵌套;右侧顶部显示当前一级目录标题,右侧目录与菜单统一不显示图标,一级目录过多时可通过上下箭头滚动,标题超长自动省略并通过黑色气泡 Tooltip 查看完整内容。普通菜单展开时无图标,收起侧栏时显示统一的页面占位图标;目录图标来自配置或默认目录图标。Portal 可通过 `portalSlots.workbar` 和 `portalSlots.toolbar` 扩展或替换工具区。 +`layout.preset` 可选 `sidebar`、`topbar`、`blank`、`dashboard`、`mobile`;`tabs` 和 `breadcrumb` 独立配置。Sidebar 顶部边界按钮负责收起/展开,展开时位于侧栏与内容区交界处,收起为 60px 图标栏后居中在栏内并只显示图标;底部始终保留菜单配置入口,展开状态另外提供隐藏、收藏、最近使用和按配置启用的搜索。菜单配置中的“顶部展示搜索按钮”开启后,复用底部搜索弹窗组件,在通知按钮左侧显示搜索按钮,并移除底部重复按钮;“隐藏菜单栏”只关闭菜单栏。收藏和最近使用由菜单 Store 管理,并按门户 Code 与环境隔离写入 `localStorage`,收藏上限 100 条、最近使用上限 10 条。开启 Tabs 后,页签顺序和当前页按同一 scope 写入 `sessionStorage`;语言、主题、时区和方向也只在当前会话内按 Portal/环境隔离。可选双栏模式,目录标题选项仅双栏模式有效。双栏模式的一级目录显示在左侧图标栏,一级目录下的目录和菜单可以同级,目录还可以继续嵌套;右侧顶部显示当前一级目录标题,右侧目录与菜单统一不显示图标,一级目录过多时可通过上下箭头滚动,标题超长自动省略并通过黑色气泡 Tooltip 查看完整内容。普通菜单展开时无图标,收起侧栏时显示统一的页面占位图标;目录图标来自配置或默认目录图标。Portal 可通过 `portalSlots.workbar` 和 `portalSlots.toolbar` 扩展或替换工具区。 Custom React 是第四种模式:`projectType: "APP"`、`framework: "react"`、`layout.preset: "custom"`。它不渲染官方导航,项目自行定义页面和外壳,但仍可使用 Runtime Context、错误边界、默认首页、认证出口、更新检测和跨应用事件总线。完整示例见 `examples/dev-demo/apps/layout-custom`。认证能力默认开启;需要完全关闭基座登录/注册入口时配置 `auth: { enabled: false }`,项目可以自行实现认证页面。 diff --git a/packages/preset/README.md b/packages/preset/README.md index d201bf9..e6d68d5 100644 --- a/packages/preset/README.md +++ b/packages/preset/README.md @@ -35,7 +35,7 @@ Choose a preset instead of copying foundation layout or responsive styles into a ### Sidebar controls -The Sidebar preset starts expanded. Its top-edge control collapses the full-width menu into a 60px icon rail. The rail has one footer restore button; fully hidden navigation restores through the top control outside the inert sidebar. Expanded navigation retains hide/settings/favorites/recent/search actions. All official Portal and APP shells share LayoutFrame; `lockSidebar` and controlled `hideSidebar`/`collapseSidebar` overrides remain supported. +The Sidebar preset starts expanded. Its top-edge control sits at the sidebar/content seam while expanded and centers inside the 60px icon rail when collapsed, showing only the expand icon. Menu settings remain in the footer in both states, while hide/favorites/recent/search actions are available when expanded. Fully hidden navigation restores through the same top control outside the inert sidebar. All official Portal and APP shells share LayoutFrame; `lockSidebar` and controlled `hideSidebar`/`collapseSidebar` overrides remain supported. See the architecture guide in https://github.com/biugle/biu/blob/main/docs/design.md. diff --git a/packages/preset/src/layout-frame.tsx b/packages/preset/src/layout-frame.tsx index 22a4928..6c512c1 100644 --- a/packages/preset/src/layout-frame.tsx +++ b/packages/preset/src/layout-frame.tsx @@ -645,7 +645,6 @@ export function LayoutFrame({ locale={locale} layoutOverrides={layoutOverrides} effectiveSidebarCollapsed={effectiveSidebarCollapsed} - collapsedControl={sidebarControl} favorites={favorites} recent={recent} clearRecent={clearRecent} @@ -779,8 +778,8 @@ export function LayoutFrame({ {sidebarFooter} - {/* Hidden navigation restores from the outside; the 60px rail uses its single footer control. */} - {!effectiveSidebarCollapsed && sidebarControl} + {/* Keep the collapse/expand control on the sidebar's top edge for expanded, collapsed and hidden states. */} + {sidebarControl}
{header} {content} diff --git a/packages/preset/src/layout-sidebar-footer.tsx b/packages/preset/src/layout-sidebar-footer.tsx index 3b464bb..7777570 100644 --- a/packages/preset/src/layout-sidebar-footer.tsx +++ b/packages/preset/src/layout-sidebar-footer.tsx @@ -1,4 +1,3 @@ -import type { ReactNode } from "react"; import { useBiuI18n } from "@biugle/biu-runtime"; import { type BiuMenuRecord, type MenuNode } from "@biugle/biu-router"; import { Tooltip } from "@biugle/react-components"; @@ -12,7 +11,6 @@ import { type LayoutSidebarFooterProps = Pick & { effectiveSidebarCollapsed: boolean; - collapsedControl: ReactNode; favorites: BiuMenuRecord[]; recent: BiuMenuRecord[]; clearRecent: () => void; @@ -31,7 +29,6 @@ export function LayoutSidebarFooter({ locale, layoutOverrides, effectiveSidebarCollapsed, - collapsedControl, favorites, recent, clearRecent, @@ -47,7 +44,7 @@ export function LayoutSidebarFooter({ const { $t } = useBiuI18n(); return (
- {/* The collapsed rail has one restore action shared by every official layout. */} + {/* Menu settings stay in the footer while the top edge control handles collapse and expansion. */} {!effectiveSidebarCollapsed && ( @@ -27,7 +27,7 @@ The root entry exports both the composable `Form` API and the higher-level not a second package boundary, so applications do not need to coordinate two Form implementations or two style entry points. -The render context exposes `field`, `fieldState` and `formState`, so custom controls and validation messages remain explicit. `Form.Item.tooltip` accepts a `ReactNode` and renders a question-mark action immediately after the label; hovering or focusing it opens the shared Tooltip without changing the field control geometry. This is independent from `help` and `extra`, which remain the validation and label-adjacent text slots. +The render context exposes `field`, `fieldState` and `formState`. `Form.Item` renders its field validation message once and sets `aria-invalid` through `field`; do not also pass the same message to a control's `error` prop, which renders its own message. Use `noStyle` when the application needs to render a custom validation message. `Form.Item.tooltip` accepts a `ReactNode` and renders a question-mark action immediately after the label; hovering or focusing it opens the shared Tooltip without changing the field control geometry. This is independent from `help` and `extra`, which remain the validation and label-adjacent text slots. `Form.Group` provides a titled business section with a responsive two-column layout by default. It keeps the form regions visually consistent while preserving the same `Form.Item` render-props contract: diff --git a/packages/react-form/test/form-functional.test.ts b/packages/react-form/test/form-functional.test.ts index 6d55338..09232e4 100644 --- a/packages/react-form/test/form-functional.test.ts +++ b/packages/react-form/test/form-functional.test.ts @@ -6,6 +6,7 @@ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { useForm } from "react-hook-form"; import { JSDOM } from "jsdom"; +import { TextField } from "@biugle/react-components"; import { Form, FormGroup, FormItem, type FormRenderContext } from "../src/index.js"; const dom = new JSDOM("", { url: "http://localhost/" }); @@ -81,13 +82,13 @@ function FormHarness({ onInvalid }: { onInvalid: (errors: unknown) => void }) { label: "名称", required: true, children: ({ field }: FormRenderContext) => - React.createElement("input", { ...field, type: "text" }), + React.createElement(TextField, { ...field, type: "text" }), }), React.createElement("button", { type: "submit" }, "提交"), ); } -test("Form.Item required creates validation and accessible field metadata", async () => { +test("Form.Item and TextField render one required validation message and accessible field metadata", async () => { const host = document.createElement("div"); document.body.append(host); let root!: Root; @@ -106,7 +107,9 @@ test("Form.Item required creates validation and accessible field metadata", asyn await Promise.resolve(); }); assert.ok(invalid); - assert.ok(host.querySelector('[role="alert"]')); + assert.equal(host.querySelectorAll('[role="alert"]').length, 1); + assert.equal(host.querySelectorAll(".biu-ui-control__error").length, 0); + assert.equal(host.querySelector(".biu-form-item")?.getAttribute("data-invalid"), "true"); assert.ok(input.getAttribute("aria-describedby")); } finally { await act(async () => root.unmount()); From 3af6bda52924a1462c3ee4e28abf6f760e78cbe1 Mon Sep 17 00:00:00 2001 From: bex Date: Mon, 2 Nov 2026 14:46:33 +0800 Subject: [PATCH 5/6] fix: support tree row selection and expansion --- .../biu-foundation-components-capabilities.md | 2 +- .../biu-foundation-components-capabilities.md | 2 +- .../apps/main-a/public/demo-locale.json | 24 ++++- .../CapabilityPage/api-component-details.ts | 12 +++ .../ComponentUIShowcase/api/navigation.ts | 16 +++ .../sections/navigation-sections.tsx | 22 +++- .../react-components/src/pro/tree/index.ts | 2 +- .../src/ui/navigation/tree.tsx | 101 +++++++++++------- .../react-components/src/ui/tree/index.ts | 2 +- .../react-components/src/ui/tree/styles.css | 5 + .../react-components/test/navigation.test.ts | 98 +++++++++++++++++ 11 files changed, 240 insertions(+), 46 deletions(-) diff --git a/docs/biu-foundation-components-capabilities.md b/docs/biu-foundation-components-capabilities.md index 9ea6d8b..4440b95 100644 --- a/docs/biu-foundation-components-capabilities.md +++ b/docs/biu-foundation-components-capabilities.md @@ -165,7 +165,7 @@ Button 在鼠标或键盘激活后必须保留 focus,并与 hover、active、f ### 4.4 数据展示与文件 - `Pagination`:页大小选择、quick jumper、showTotal、itemRender、responsive、simple、hideOnSinglePage、showLessItems 和完整页码布局。页大小必须使用组件库 `Select` 的 small 尺寸,quick jumper 必须使用组件库 `TextField` 的 small 尺寸,禁止混用原生输入控件。`quickJumperWidth` 默认 `90px`,接受数字像素值或 CSS 宽度字符串,并同时控制页大小下拉与跳页输入框;`responsive=true` 时小于 `560px` 只显示当前页,`560–799px` 保留完整页码但隐藏可选的范围/总数,两档下拉和跳页输入框统一为 `45px`;`800px` 起使用标准布局及配置宽度。基础 Pagination 与 Table Pagination 使用同一契约,`responsive=false` 可关闭视口适配。 -- `Tree`:搜索、展开、选择/多选、checkable、半选、禁用、异步加载、层级引导线、目录/文件图标、Ellipsis 和性能边界。 +- `Tree`:默认点击节点整行展开/折叠并单选当前节点;`multiple` 显式开启多选,`expandOnRowClick` 可关闭整行展开,`onRowClick` 可观察或通过 `event.preventDefault()` 接管默认选择/展开;搜索、checkable、半选、禁用、异步加载、层级引导线、目录/文件图标、Ellipsis 和性能边界。 - `Transfer`:双栏、搜索、全选、禁用、标题、统计、长文本 Ellipsis、批量移动和受控 target;`oneWay` 控制单向或双向移动,`itemActions(item, direction)` 自定义条目最右侧操作区。双向模式右侧未自定义操作时默认显示独立删除按钮,点击后将条目移回左侧;单向模式不渲染移回操作。组件展示页同时提供普通双向、后端未知目标项和 `oneWay` 单向三个可操作案例。视口不超过 640px 时两个列表上下排列;宽屏下仅当组件容器不超过 480px 才采用窄屏布局,按钮在列表间横向排列并使用下/上箭头;有足够空间时列表并排、操作按钮纵向排列并使用左右箭头。这样双栏 Demo 即使在较宽屏幕上被放入半宽卡片,也不会过早切换为竖排。 - `Alert/Empty/Loading/Progress/Tag/Badge/Steps`:标准语义色、可替换 icon、关闭、action、空态、加载、进度、轻量状态和步骤流程。Alert 支持显式 `dir="ltr" | "rtl"`,默认固定 LTR;Alert、Notification 和 `biuMessage` 都由调用方逐实例传入方向,不自动跟随 Runtime 或 ConfigProvider。RTL 下语义边线、图标、正文、操作和关闭区域沿阅读方向排列;Notification 堆栈放在所选方向的行内末端。Alert 与 Message 都提供多行案例,关闭按钮独占逻辑末端并垂直居中;Steps 数字与文本垂直居中。 - `Upload/FilePreview/FileCard/FileRender`:拖拽/选择、accept、大小校验、multiple、进度、取消、重试、移除、下载、图片/PDF/Markdown 预览、文件大小和状态展示;文本预览禁用 iframe 脚本,PDF 预览只允许隔离源中的脚本,所有预览 iframe 不携带 referrer;危险脚本 URL 不作为文件源加载。预览与下载动作统一 small 尺寸,下载链接不显示浏览器默认下划线,下载文字在按钮框内视觉居中;文件项的图标、文件名与实际存在的元信息垂直居中,无元信息时不保留空行。 diff --git a/docs/en/biu-foundation-components-capabilities.md b/docs/en/biu-foundation-components-capabilities.md index 58f136b..44ab555 100644 --- a/docs/en/biu-foundation-components-capabilities.md +++ b/docs/en/biu-foundation-components-capabilities.md @@ -56,7 +56,7 @@ There is one component capability page. It uses one full-width section per compo - Select also exposes `size="small" | "medium" | "large"` so PageFilter and Toolbar controls share the TextField/Button baseline. - Slider supports a single value or a `range` tuple, decimal-safe `min`/`max`/`step` snapping, `marks`, `dots`, `tooltip="hover" | "always" | "never"`, `opacity` from 0 to 1, vertical/reverse layouts, disabled state, controlled/uncontrolled values and `onChange`/`onChangeComplete`. `hover` shows the current value while hovering, focusing or actively dragging a handle on touch, `always` keeps it visible, and `never` hides it; the legacy `auto` value is treated as `hover`. Native range inputs provide keyboard and accessibility semantics; the visible rail and native handle share the same endpoints, and value tooltips use a light surface. The component showcase demonstrates weather ranges, volume, brightness, rating and opacity, while the Form showcase binds scalar and range values through `Form.Item`. - Date/Time components support `YYYY-MM-DD`, `YYYY-MM-DD HH:mm:ss`, readable format aliases, dayjs, ranges, presets, disabled date/time, needConfirm, footer, 12/24-hour mode and Chinese/English resources. The calendar header accepts a directly entered year and a separately selected month while retaining previous/next navigation; `min/max` constrain dates, months and navigation boundaries. Single-date presets default to Today/Tomorrow/Yesterday, date-range presets default to Last week/Last month, and callers can override either with `presets`. Date, time, preset, Today and clear actions update the value and call `onChange` immediately without closing the panel. `needConfirm` only adds a button that closes the panel; confirming never emits the same value again. The trigger, outside click and Escape also close it. Calendar shortcuts navigate to their selected date; when a range fits in one calendar, its end date determines the focused month. Range date panels use a direct two-click flow without an extra start/end selector, report the incomplete start immediately and normalize reversed selections. Range time panels keep an existing opposite value or fill the value currently shown by the opposite dropdown when one side is edited. Range panels use equal sides with a centered em dash; time presets default to “Last hour” and “Last day”. The time-range main area shrinks to its container without a desktop horizontal panel scrollbar; on narrow viewports, the right hour/minute/second controls stay inside the panel. The component demos include `YYYY-MM-DD hh:mm A` date-time and `hh:mm A` time examples alongside the 24-hour formats. -- Dialog/Drawer, Tooltip/Ellipsis, Popover/Popconfirm, DropdownMenu, Tabs, Menu/Breadcrumb, Pagination, Tree, Transfer, Alert/Empty/Loading/Progress/Tag/Badge/Steps and Upload/File Preview follow the same portal, focus, collision, cursor, class and locale contracts. Popover keeps a comfortable reading surface by default (180px minimum, 420px maximum, 12px padding and 10px radius); Popconfirm and DropdownMenu remain compact. Only the inner long-menu list scrolls; overlay shells do not become scroll containers. UI and Pro Drawer default to the right placement when `placement` is omitted; the Platform capability Demo declares `placement="right"` explicitly. Pagination page-size selection must use the library `Select` in small size, and quick jump must use the library `TextField` in small size; native input controls are not allowed. `quickJumperWidth` defaults to `90px` and accepts a number of pixels or a CSS width string for both Pagination implementations, controlling both the page-size Select and quick-jump TextField. With `responsive=true`, below 560px the page list collapses to the current page; from 560–799px the full page list remains while optional range/total text is hidden; both bands use 45px page-size and jump controls. At 800px and wider the standard layout and configured width apply. `responsive=false` disables viewport adaptation. File rows vertically center the icon, filename and any present metadata, without reserving an empty metadata line. Transfer supports `oneWay` for one-way or two-way movement; `itemActions(item, direction)` owns the far-right item action slot, with a default remove action on the right in two-way mode. The showcase includes ordinary two-way, unknown backend target, and one-way Transfer cases. Viewports up to 640px stack the lists vertically; on wider viewports, the container switches to the narrow layout only at 480px or less. In the compact layout, both transfer buttons share one row with down/up arrows; with enough component width, lists stay side by side and actions stay in a vertical row with left/right arrows. This keeps paired showcase cards horizontal on wide screens. +- Dialog/Drawer, Tooltip/Ellipsis, Popover/Popconfirm, DropdownMenu, Tabs, Menu/Breadcrumb, Pagination, Tree (rows select the clicked node and expand/collapse by default; `multiple` enables multi-select, `expandOnRowClick` disables row expansion, and `onRowClick` can cancel default selection and expansion with `event.preventDefault()`), Transfer, Alert/Empty/Loading/Progress/Tag/Badge/Steps and Upload/File Preview follow the same portal, focus, collision, cursor, class and locale contracts. Popover keeps a comfortable reading surface by default (180px minimum, 420px maximum, 12px padding and 10px radius); Popconfirm and DropdownMenu remain compact. Only the inner long-menu list scrolls; overlay shells do not become scroll containers. UI and Pro Drawer default to the right placement when `placement` is omitted; the Platform capability Demo declares `placement="right"` explicitly. Pagination page-size selection must use the library `Select` in small size, and quick jump must use the library `TextField` in small size; native input controls are not allowed. `quickJumperWidth` defaults to `90px` and accepts a number of pixels or a CSS width string for both Pagination implementations, controlling both the page-size Select and quick-jump TextField. With `responsive=true`, below 560px the page list collapses to the current page; from 560–799px the full page list remains while optional range/total text is hidden; both bands use 45px page-size and jump controls. At 800px and wider the standard layout and configured width apply. `responsive=false` disables viewport adaptation. File rows vertically center the icon, filename and any present metadata, without reserving an empty metadata line. Transfer supports `oneWay` for one-way or two-way movement; `itemActions(item, direction)` owns the far-right item action slot, with a default remove action on the right in two-way mode. The showcase includes ordinary two-way, unknown backend target, and one-way Transfer cases. Viewports up to 640px stack the lists vertically; on wider viewports, the container switches to the narrow layout only at 480px or less. In the compact layout, both transfer buttons share one row with down/up arrows; with enough component width, lists stay side by side and actions stay in a vertical row with left/right arrows. This keeps paired showcase cards horizontal on wide screens. - Alert and Message both demonstrate multiline content. Their action/close areas use independent grid columns and never overlap; Message supports bounded viewport dragging through `draggable`, while narrow Alerts move the action below the content and keep close at the logical end (right in LTR, left in RTL). - Dialog is draggable by default from a non-interactive area of its header, clamped to the browser viewport; closing and reopening restores its configured initial position, `draggable={false}` disables movement, and Drawer remains non-draggable by default. Menu, Alert, Notification, `biuMessage`, Table and ProTable accept an explicit per-instance `dir="ltr" | "rtl"` and default to fixed LTR. They do not read Runtime or ConfigProvider direction. RTL mirrors reading order and supported logical edges; Table's explicit `align="left" | "right"` remains physical. The component showcase provides manual direction switches. - PageFilter labels float over the single control border without creating a second frame. Direct controls receive `w-full`, preserve caller classes and fill equal explicit Grid tracks. `itemMinWidth: number` defaults to 170px; container width, explicit item widths and action content determine capacity, capped at six columns. `maxRows` defaults to two and normalizes to 1..2. Actions align right in the last explicit track (`-2 / -1`); `grid-column: -1` must not create an extra intrinsic-width column. `PageFilterItem.width` still opts into a fixed item width bounded by its container. Container, field configuration, locale and action-size changes recalculate capacity without mounting hidden duplicate controls. `autoCollapse=true` exposes overflow in the wider `large` Drawer; false preserves all fields and permits additional natural rows. Inline and Drawer fields share caller-controlled state; includeVisibleFieldsInDrawer=false retains only collapsed fields and moreFields. An open Drawer survives window expansion; closing removes an unnecessary entry and narrowing never reopens it automatically. The Drawer has two columns, or one on mobile; narrow one-row layouts may keep only actions and expose all fields through the Drawer. Empty filters reserve no space. More filters use secondary outlined, reset uses secondary contained, and query uses dark contained, all small with the shared Ellipsis/Tooltip handling for long labels. FormGroup retains title, icon, description, border, columns, gap and slot overrides. diff --git a/examples/dev-demo/apps/main-a/public/demo-locale.json b/examples/dev-demo/apps/main-a/public/demo-locale.json index 59f9792..3f1c0f5 100644 --- a/examples/dev-demo/apps/main-a/public/demo-locale.json +++ b/examples/dev-demo/apps/main-a/public/demo-locale.json @@ -2104,7 +2104,7 @@ "标题锚点 Anchor / 滚动进度 ScrollProgress": "标题锚点 Anchor / 滚动进度 ScrollProgress", "树 Tree": "树 Tree", "树节点": "树节点", - "树节点展开、选择、多选和禁用状态。": "树节点展开、选择、多选和禁用状态。", + "默认点击整行可展开或折叠并单选当前行;multiple 可开启多选,勾选状态独立控制。": "默认点击整行可展开或折叠并单选当前行;multiple 可开启多选,勾选状态独立控制。", "样式扩展": "样式扩展", "根节点和区域 class": "根节点和区域 class", "根节点样式": "根节点样式", @@ -2334,6 +2334,16 @@ "节点 key 数组": "节点 key 数组", "节点和关键词 / 节点": "节点和关键词 / 节点", "节点展开变化后的回调。": "节点展开变化后的回调。", + "点击节点行时切换展开/折叠;设置为 false 后仍可点击展开图标。": "点击节点行时切换展开/折叠;设置为 false 后仍可点击展开图标。", + "点击节点行时触发(复选框除外);调用 event.preventDefault() 可取消默认选中和展开行为。": "点击节点行时触发(复选框除外);调用 event.preventDefault() 可取消默认选中和展开行为。", + "当前节点和行点击事件": "当前节点和行点击事件", + "点击节点行时切换展开/折叠;设为 false 后,展开图标仍可切换节点。": "点击节点行时切换展开/折叠;设为 false 后,展开图标仍可切换节点。", + "节点行点击回调;调用 event.preventDefault() 可取消组件默认的选择和展开动作。": "节点行点击回调;调用 event.preventDefault() 可取消组件默认的选择和展开动作。", + "当前 TreeNode 和行点击事件": "当前 TreeNode 和行点击事件", + "整行展开": "整行展开", + "行点击回调": "行点击回调", + "最近点击节点:{node}": "最近点击节点:{node}", + "暂无": "暂无", "节点折叠层级": "节点折叠层级", "节点折叠状态": "节点折叠状态", "节点类型": "节点类型", @@ -5657,7 +5667,7 @@ "标题锚点 Anchor / 滚动进度 ScrollProgress": "Anchor / ScrollProgress", "树 Tree": "Tree", "树节点": "Tree node", - "树节点展开、选择、多选和禁用状态。": "Tree-node expansion, selection, multiple selection, and disabled states.", + "默认点击整行可展开或折叠并单选当前行;multiple 可开启多选,勾选状态独立控制。": "By default, clicking a row selects only that node and expands or collapses it; set multiple to enable multi-selection. Checkbox state is controlled separately.", "样式扩展": "Style extensions", "根节点和区域 class": "Root and region class values", "根节点样式": "Root styles", @@ -5888,6 +5898,16 @@ "节点 key 数组": "Array of node keys", "节点和关键词 / 节点": "Node and keyword / Node", "节点展开变化后的回调。": "Callback after node expansion changes.", + "点击节点行时切换展开/折叠;设置为 false 后仍可点击展开图标。": "Clicking a node row toggles expansion; the expand icon remains available when this is false.", + "点击节点行时触发(复选框除外);调用 event.preventDefault() 可取消默认选中和展开行为。": "Called when a node row is clicked, except for its checkbox; call event.preventDefault() to cancel default selection and expansion.", + "当前节点和行点击事件": "Current node and row click event", + "点击节点行时切换展开/折叠;设为 false 后,展开图标仍可切换节点。": "Clicking a node row toggles expansion; the expand icon remains available when this is false.", + "节点行点击回调;调用 event.preventDefault() 可取消组件默认的选择和展开动作。": "Node row click callback; call event.preventDefault() to cancel the default selection and expansion actions.", + "当前 TreeNode 和行点击事件": "Current TreeNode and row click event", + "整行展开": "Expand from the row", + "行点击回调": "Row click callback", + "最近点击节点:{node}": "Last clicked node: {node}", + "暂无": "None yet", "节点折叠层级": "Node collapse depth", "节点折叠状态": "Node collapsed state", "节点类型": "Node type", diff --git a/examples/dev-demo/apps/main-a/src/pages/CapabilityPage/api-component-details.ts b/examples/dev-demo/apps/main-a/src/pages/CapabilityPage/api-component-details.ts index 75dd03b..e5fe8e7 100644 --- a/examples/dev-demo/apps/main-a/src/pages/CapabilityPage/api-component-details.ts +++ b/examples/dev-demo/apps/main-a/src/pages/CapabilityPage/api-component-details.ts @@ -55,6 +55,18 @@ export const componentApiPropertyDetails: Record) => void", + defaultValue: "undefined", + options: "当前 TreeNode 和行点击事件", + description: "点击节点行时触发(复选框除外);调用 event.preventDefault() 可取消组件默认的选择和展开动作。", + }, "Alert.onClose": { type: "() => void", defaultValue: "undefined", diff --git a/examples/dev-demo/apps/main-a/src/pages/ComponentUIShowcase/api/navigation.ts b/examples/dev-demo/apps/main-a/src/pages/ComponentUIShowcase/api/navigation.ts index b3164e1..2bfd406 100644 --- a/examples/dev-demo/apps/main-a/src/pages/ComponentUIShowcase/api/navigation.ts +++ b/examples/dev-demo/apps/main-a/src/pages/ComponentUIShowcase/api/navigation.ts @@ -239,6 +239,22 @@ export const navigationApiRows: CapabilityApiRow[] = [ ["onSelect", "(keys: string[]) => void", "undefined", "节点 key 数组", "节点选中变化后的回调。", "联动"], ["expandedKeys / defaultExpandedKeys", "string[]", "[]", "节点 key 数组", "控制受控或非受控展开节点。", "展开"], ["onExpand", "(keys: string[]) => void", "undefined", "节点 key 数组", "节点展开变化后的回调。", "展开联动"], + [ + "expandOnRowClick", + "boolean", + "true", + "true | false", + "点击节点行时切换展开/折叠;设置为 false 后仍可点击展开图标。", + "整行展开", + ], + [ + "onRowClick", + "(node: TreeNode, event: React.MouseEvent) => void", + "undefined", + "当前节点和行点击事件", + "点击节点行时触发(复选框除外);调用 event.preventDefault() 可取消默认选中和展开行为。", + "行点击回调", + ], ["checkedKeys / defaultCheckedKeys", "string[]", "[]", "节点 key 数组", "控制受控或非受控勾选节点。", "勾选"], [ "onCheck", diff --git a/examples/dev-demo/apps/main-a/src/pages/ComponentUIShowcase/sections/navigation-sections.tsx b/examples/dev-demo/apps/main-a/src/pages/ComponentUIShowcase/sections/navigation-sections.tsx index e9f6249..61a034b 100644 --- a/examples/dev-demo/apps/main-a/src/pages/ComponentUIShowcase/sections/navigation-sections.tsx +++ b/examples/dev-demo/apps/main-a/src/pages/ComponentUIShowcase/sections/navigation-sections.tsx @@ -2,6 +2,7 @@ import { useState, type ReactNode } from "react"; import { Breadcrumb, Button, + Checkbox, DropdownMenu, Menu, Pagination, @@ -29,6 +30,9 @@ export function NavigationSections({ pro, showcaseDirection, setShowcaseDirectio const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); const [treeSelected, setTreeSelected] = useState(["control"]); + const [treeExpanded, setTreeExpanded] = useState(["root", "ui"]); + const [treeMultiple, setTreeMultiple] = useState(false); + const [lastTreeRowClick, setLastTreeRowClick] = useState(""); const [transfer, setTransfer] = useState>([ { label: "Runtime", value: "runtime" }, ]); @@ -187,7 +191,16 @@ export function NavigationSections({ pro, showcaseDirection, setShowcaseDirectio locale={locale} /> -
+
+ setTreeMultiple(event.currentTarget.checked)} + /> setLastTreeRowClick(node.key)} + multiple={treeMultiple} checkable defaultCheckedKeys={["ui"]} showSearch locale={locale} /> + {$t("最近点击节点:{node}", { node: lastTreeRowClick || $t("暂无") })}
void; + onRowClick?: (node: TreeNode, event: React.MouseEvent) => void; + className?: string; + multiple?: boolean; + defaultExpandedKeys?: string[]; + expandedKeys?: string[]; + onExpand?: (keys: string[]) => void; + expandOnRowClick?: boolean; + checkable?: boolean; + checkStrictly?: boolean; + checkedKeys?: string[]; + defaultCheckedKeys?: string[]; + onCheck?: (keys: string[], info: { node: TreeNode; checked: boolean; halfCheckedKeys: string[] }) => void; + showSearch?: boolean; + searchValue?: string; + defaultSearchValue?: string; + onSearch?: (value: string) => void; + filterTreeNode?: (node: TreeNode, searchValue: string) => boolean; + titleRender?: (node: TreeNode) => React.ReactNode; + loadData?: (node: TreeNode) => TreeNode[] | void | Promise; + onLoadError?: (error: unknown, node: TreeNode) => void; + classNames?: TreeClassNames; +} + function collectDescendantKeys(node: TreeNode, childrenFor: (node: TreeNode) => TreeNode[]): string[] { return childrenFor(node).flatMap((child) => [child.key, ...collectDescendantKeys(child, childrenFor)]); } @@ -35,11 +63,13 @@ export function Tree({ selectedKeys, defaultSelectedKeys = [], onSelect, + onRowClick, className, multiple = false, defaultExpandedKeys = [], expandedKeys, onExpand, + expandOnRowClick = true, checkable = false, checkStrictly = false, checkedKeys, @@ -56,31 +86,7 @@ export function Tree({ classNames, locale, localeText, -}: { - data: TreeNode[]; - selectedKeys?: string[]; - defaultSelectedKeys?: string[]; - onSelect?: (keys: string[]) => void; - className?: string; - multiple?: boolean; - defaultExpandedKeys?: string[]; - expandedKeys?: string[]; - onExpand?: (keys: string[]) => void; - checkable?: boolean; - checkStrictly?: boolean; - checkedKeys?: string[]; - defaultCheckedKeys?: string[]; - onCheck?: (keys: string[], info: { node: TreeNode; checked: boolean; halfCheckedKeys: string[] }) => void; - showSearch?: boolean; - searchValue?: string; - defaultSearchValue?: string; - onSearch?: (value: string) => void; - filterTreeNode?: (node: TreeNode, searchValue: string) => boolean; - titleRender?: (node: TreeNode) => React.ReactNode; - loadData?: (node: TreeNode) => TreeNode[] | void | Promise; - onLoadError?: (error: unknown, node: TreeNode) => void; - classNames?: TreeClassNames; -} & NavigationLocaleProps) { +}: TreeProps) { const text = useComponentsLocale(locale, localeText); const [internalExpanded, setInternalExpanded] = React.useState(defaultExpandedKeys); const [internalSelected, setInternalSelected] = React.useState(selectedKeys ?? defaultSelectedKeys); @@ -166,6 +172,31 @@ export function Tree({ // resolve the original node so checking an ancestor still includes hidden // descendants that were excluded from the visible search result. const selectionNode = (node: TreeNode) => findNode(data, node.key) ?? node; + const handleRowClick = (node: TreeNode, event: React.MouseEvent) => { + if (node.disabled) return; + const sourceNode = selectionNode(node); + onRowClick?.(sourceNode, event); + if (event.defaultPrevented) return; + + const canExpand = childrenFor(node).length > 0 || (Boolean(loadData) && !node.isLeaf); + const clickedToggle = (event.target as Element).closest(".biu-ui-tree__toggle") !== null; + if (clickedToggle) { + if (canExpand && !loadingKeys.includes(node.key)) void toggle(node); + return; + } + + const next = multiple + ? activeSelected.includes(node.key) + ? activeSelected.filter((key) => key !== node.key) + : [...activeSelected, node.key] + : [node.key]; + if (selectedKeys === undefined) setInternalSelected(next); + onSelect?.(next); + + if (canExpand && expandOnRowClick && !loadingKeys.includes(node.key)) { + void toggle(node); + } + }; const expandedForRender = React.useMemo( () => (activeSearch.trim() ? new Set([...activeExpanded, ...searchExpandedKeys]) : new Set(activeExpanded)), [activeExpanded, activeSearch, searchExpandedKeys], @@ -175,8 +206,14 @@ export function Tree({ {nodes.map((node) => (
  • handleRowClick(node, event)} > {childrenFor(node).length || (loadData && !node.isLeaf) ? ( + {$t("切换到其他菜单再返回,计数应保留。")} {storeState.selected.length ? ( {$t("已选 {count} 条", { count: storeState.selected.length })} ) : null} diff --git a/examples/dev-demo/apps/vue-child/public/demo-locale.json b/examples/dev-demo/apps/vue-child/public/demo-locale.json index 47b24b9..df26f07 100644 --- a/examples/dev-demo/apps/vue-child/public/demo-locale.json +++ b/examples/dev-demo/apps/vue-child/public/demo-locale.json @@ -13,6 +13,9 @@ "主题": "主题", "布局方向": "布局方向", "页面 Code": "页面 Code", + "宿主页面状态": "宿主页面状态", + "当前激活": "当前激活", + "后台保留中": "后台保留中", "登录状态": "登录状态", "已登录": "已登录", "未登录": "未登录", @@ -35,6 +38,9 @@ "主题": "Theme", "布局方向": "Layout direction", "页面 Code": "Page Code", + "宿主页面状态": "Host route status", + "当前激活": "Active", + "后台保留中": "Kept alive in background", "登录状态": "Sign-in status", "已登录": "Signed in", "未登录": "Signed out", diff --git a/examples/dev-demo/apps/vue-child/src/adapters/vue.ts b/examples/dev-demo/apps/vue-child/src/adapters/vue.ts index 07e5c2b..6d3bfb7 100644 --- a/examples/dev-demo/apps/vue-child/src/adapters/vue.ts +++ b/examples/dev-demo/apps/vue-child/src/adapters/vue.ts @@ -1,5 +1,8 @@ -import { createApp, type Component } from "vue"; +import { createApp, reactive, type Component } from "vue"; import type { BiuFrameworkAdapter } from "@biugle/biu-runtime"; +import type { BiuPageContext } from "@biugle/biu-runtime"; + +const mountedPages = new WeakMap; unmount: () => void }>(); const vueAdapter: BiuFrameworkAdapter = { framework: "vue", @@ -9,9 +12,23 @@ const vueAdapter: BiuFrameworkAdapter = { }, renderPage(container, page, context) { if (!page || typeof page !== "object") throw new Error(`Vue 页面模块无效:${context.code}`); - const app = createApp(page as Component, { context }); + const contextState = reactive({ ...context }) as Record; + const app = createApp(page as Component, { context: contextState }); app.mount(container); - return () => app.unmount(); + const unmount = () => { + app.unmount(); + mountedPages.delete(container); + }; + mountedPages.set(container, { page, context: contextState, unmount }); + return unmount; + }, + updatePage(container, page, context: BiuPageContext) { + const mounted = mountedPages.get(container); + if (!mounted || mounted.page !== page) return; + for (const key of Object.keys(mounted.context)) { + if (!(key in context)) delete mounted.context[key]; + } + Object.assign(mounted.context, context); }, }; diff --git a/examples/dev-demo/apps/vue-child/src/pages/VuePage/index.vue b/examples/dev-demo/apps/vue-child/src/pages/VuePage/index.vue index f3fb9f3..da8c0b8 100644 --- a/examples/dev-demo/apps/vue-child/src/pages/VuePage/index.vue +++ b/examples/dev-demo/apps/vue-child/src/pages/VuePage/index.vue @@ -11,6 +11,7 @@ interface PageContext { theme?: string; direction?: string; code?: string; + hostActive?: boolean; auth?: { authenticated: boolean; user?: { name?: string } }; } @@ -18,6 +19,13 @@ const props = defineProps<{ context?: PageContext }>(); const count = ref(0); const $t = useDemoLocale(() => props.context?.locale); const hostName = computed(() => props.context?.portalCode || $t("独立运行")); +const hostActiveLabel = computed(() => + props.context?.hostActive === undefined + ? $t("独立运行") + : props.context.hostActive + ? $t("当前激活") + : $t("后台保留中"), +);