feat(layout): add layout component#364
Conversation
|
Warning Review limit reached
More reviews will be available in 37 minutes and 58 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughIntroduces a complete ChangesLayout Component System
Sequence Diagram(s)sequenceDiagram
participant User
participant Layout_vue as Layout.vue
participant LayoutSurface
participant FloatingDragBar
participant FloatingResizeTriggers
participant AsideResizeTrigger
rect rgba(100, 149, 237, 0.5)
note over User,AsideResizeTrigger: Aside resize flow
User->>AsideResizeTrigger: pointerdown
AsideResizeTrigger->>AsideResizeTrigger: lockBodyInteraction + capture pointer
AsideResizeTrigger->>Layout_vue: aside-resize-start(detail)
AsideResizeTrigger->>Layout_vue: width-change(value) via rAF
AsideResizeTrigger->>Layout_vue: aside-resize-end(detail)
end
rect rgba(144, 238, 144, 0.5)
note over User,FloatingDragBar: Floating drag flow
User->>FloatingDragBar: pointerdown
FloatingDragBar->>LayoutSurface: drag-start(rect)
FloatingDragBar->>LayoutSurface: drag(rect)
FloatingDragBar->>LayoutSurface: drag-end(rect)
LayoutSurface->>Layout_vue: floating-drag-start/drag/drag-end + floating-state-change
end
rect rgba(255, 165, 0, 0.5)
note over User,FloatingResizeTriggers: Floating resize flow
User->>FloatingResizeTriggers: pointerdown (handle)
FloatingResizeTriggers->>FloatingResizeTriggers: resolveFloatingResizeRect + clampFloatingRectByHandle
FloatingResizeTriggers->>LayoutSurface: resize-start/resize/resize-end
LayoutSurface->>Layout_vue: floating-resize-start/resize/resize-end + floating-state-change
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📦 Package Previewpnpm add https://pkg.pr.new/@opentiny/tiny-robot@46e968d pnpm add https://pkg.pr.new/@opentiny/tiny-robot-kit@46e968d pnpm add https://pkg.pr.new/@opentiny/tiny-robot-svgs@46e968d commit: 46e968d |
…llbar and update layout state management
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/components/src/layout/index.type.ts`:
- Around line 74-82: The LayoutFloatingStateControlProps union type at line
74-82 has both branches requiring a property with type never, making the type
uninhabitable. Remove the never-typed properties from each branch instead: the
first branch should only require floatingState with no defaultFloatingState
property, and the second branch should only have an optional
defaultFloatingState property with no floatingState property. This creates a
proper exclusive union where one branch accepts floatingState and the other
accepts defaultFloatingState.
In `@packages/components/src/layout/LayoutAsideToggle.vue`:
- Around line 37-40: The toggle button in LayoutAsideToggle.vue lacks
accessibility attributes needed for screen readers and users relying on
assistive technology. Add an aria-label attribute to the button element with
class "tr-layout-aside-toggle" to provide an accessible name (especially
important since the default slot may contain only an icon), and add an
aria-pressed or aria-expanded attribute bound to the panel's state to convey
whether the aside panel is currently open or closed. These attributes should be
bound dynamically to reflect the current toggle state.
In `@packages/components/src/layout/utils/surfaceGeometry.ts`:
- Around line 282-288: The `resolveDefaultFloatingRect` function accepts a
`bounds` parameter but ignores it when computing constraints. Currently,
`resolveFloatingConstraints(config)` derives constraints from default viewport
bounds instead of the passed `bounds`, causing the clamped width and height
values to potentially exceed the custom bounds. Modify the function to pass the
`bounds` parameter to `resolveFloatingConstraints` (or update how constraints
are calculated) so that the width and height clamping respects the actual bounds
provided to the function rather than always defaulting to viewport bounds.
In `@packages/components/src/shared/composables/useControllableState.ts`:
- Around line 17-19: The issue is in the useControllableState composable where
internalState is only initialized once with options.defaultValue and never
updated while the component is in controlled mode. When isControlled transitions
from true to false, the resolvedState computed property falls back to the stale
internalState instead of the latest controlled value. Add a watcher that
monitors options.value and synchronizes internalState whenever isControlled is
true, ensuring that when the component transitions to uncontrolled mode,
internalState preserves the last controlled value instead of reverting to the
initial default value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 9b176ce3-faf4-4f31-98b8-33f6a4c0c3cc
📒 Files selected for processing (31)
packages/components/src/index.tspackages/components/src/layout/Layout.vuepackages/components/src/layout/LayoutAsideToggle.vuepackages/components/src/layout/LayoutProxyScrollbar.vuepackages/components/src/layout/components/AsideContent.vuepackages/components/src/layout/components/AsideResizeTrigger.vuepackages/components/src/layout/components/FloatingResizeTrigger.vuepackages/components/src/layout/composables/useLayoutAsideResize.tspackages/components/src/layout/composables/useLayoutContext.tspackages/components/src/layout/composables/useLayoutDrawerActions.tspackages/components/src/layout/composables/useLayoutFloating.tspackages/components/src/layout/composables/useLayoutFloatingDrag.tspackages/components/src/layout/composables/useLayoutFloatingResize.tspackages/components/src/layout/composables/useLayoutProxyScrollbar.tspackages/components/src/layout/composables/useLayoutRenderState.tspackages/components/src/layout/composables/useLayoutRootState.tspackages/components/src/layout/index.tspackages/components/src/layout/index.type.tspackages/components/src/layout/internal.type.tspackages/components/src/layout/utils/asideDefaults.tspackages/components/src/layout/utils/cssLength.tspackages/components/src/layout/utils/domInteraction.tspackages/components/src/layout/utils/emitAsideEvents.tspackages/components/src/layout/utils/math.tspackages/components/src/layout/utils/slots.tspackages/components/src/layout/utils/surfaceGeometry.tspackages/components/src/layout/utils/surfaceResize.tspackages/components/src/shared/composables/index.tspackages/components/src/shared/composables/useControllableState.tspackages/components/src/styles/components/index.csspackages/components/src/styles/components/layout.less
| export type LayoutFloatingResizeHandle = 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw' | ||
|
|
||
| export interface LayoutAsideOpenEventDetail { | ||
| placement: LayoutPlacement |
There was a problem hiding this comment.
placement 很容易和 floating 的 placement 混淆,建议改成 side,专门表示 left-aside 和 right-aside 的位置,其他相关的地方也需要更改
| expandedWidth: number | ||
| collapsedWidth: number | undefined | ||
| resizable: boolean | ||
| isRail: boolean |
There was a problem hiding this comment.
为什么需要 isRail,isHidden,canResize 这些变量。
isRail 我理解是要判断窄边框,完全可以 open + collapsedWidth 替代。因为就算你用了 isRail,你还是可能需要 collapsedWidth 来写布局,所以 isRail 是多余的,而且理解难度也很大,第一眼看不出来这个变量表达什么意思。isHidden 也是同理。
canResize 和 resizable 有什么区别?看不出来
There was a problem hiding this comment.
已经直接把 left-aside / right-aside 的 slotProps 移除了。
因为这组 LayoutAsideSlotProps 按照实际使用场景梳理下来算是冗余设计:
open、expandedWidth、collapsedWidth、resizable、mode 这类信息,外层本来就可以通过传入的 state 和 option 得到,没必要再从插槽重复暴露
isRail、isHidden、canResize 这类值更偏内部派生状态,也不适合暴露给内容插槽
移除后使用方式不受影响:
侧栏状态继续由外层通过 leftAside / rightAside + events 管理
插槽内部如果只需要局部开关,用 Layout.AsideToggle 即可
| isResizing: isAsideResizing, | ||
| }) | ||
|
|
||
| const { isFloating, showDragBar, floatingClass, floatingStyle, dragBarClass, resizeHandles } = useLayoutFloating({ |
There was a problem hiding this comment.
你这里把是否支持悬浮、drag的逻辑、resize的逻辑全部混合到一起了。直接下放到组件更好。组件只需要管理自己的交互逻辑
There was a problem hiding this comment.
后续没有把 drag / resize 逻辑直接放回 Layout.vue,按照下述设计进行分层
Layout.vue 负责整体布局编排和状态组织,比如 aside / drawer / floating 状态管理,以及对外事件转发。
LayoutSurface 负责 floating surface 这一层能力,包括 Teleport、浮层定位、drag、resize 和对应的 class/style。
具体交互细节由 FloatingDragBar、FloatingResizeTriggers 负责,分别处理浮层拖动、浮层缩放。
… layout management
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/components/src/layout/composables/useLayoutRootState.ts (1)
113-121:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGate resolved floating state by mode to avoid stale state after mode switches.
At Line 114,
resolvedFloatingStateis not mode-guarded. When mode changes fromfloatingtonormal, the last floating state can remain cached and keepresolvedFloatingnon-empty.Suggested fix
- const resolvedFloatingState = computed(() => floatingState.resolvedState.value) + const resolvedFloatingState = computed(() => + props.mode === 'floating' ? floatingState.resolvedState.value : undefined, + )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/components/src/layout/composables/useLayoutRootState.ts` around lines 113 - 121, The resolvedFloatingState is not mode-guarded in the resolvedFloating computed property, causing cached floating state to persist when the mode switches from floating to normal. Gate the nextFloatingState assignment by checking the mode first, similar to how nextFloatingOptions is already guarded - only retrieve floatingState.resolvedState.value when props.mode === 'floating', otherwise set nextFloatingState to undefined to prevent stale state from remaining after a mode switch.packages/components/src/layout/utils/cssLength.ts (1)
6-21:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAccept zero-valued CSS lengths with units.
At Line 6 and Line 19, values like
0rem/0%now miss the zero fast-path and incorrectly resolve tofallbackinstead of0.Suggested fix
-const ZERO_LENGTH_RE = /^0(?:\.0+)?$/i +const ZERO_LENGTH_RE = /^0(?:\.0+)?(?:[a-z%]+)?$/i🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/components/src/layout/utils/cssLength.ts` around lines 6 - 21, The ZERO_LENGTH_RE regex pattern at the top of the resolveCssLengthToPx function currently only matches plain zero values like "0" or "0.0" but does not match zero-valued CSS lengths with units such as "0rem", "0%", or "0px". When these unit-based values are tested against the regex in the function, the test fails and the function falls through to other logic instead of correctly returning 0. Update the ZERO_LENGTH_RE regex pattern to also match optional CSS units (such as rem, px, %, em, vh, vw, etc.) that may appear after the zero value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/components/src/layout/Layout.vue`:
- Around line 110-117: The computed properties leftDockWidth and rightDockWidth
are calculated unconditionally regardless of whether their corresponding aside
slots actually exist. This causes incorrect resize bounds to be applied even
when the slot is not rendered. Gate leftDockWidth to return a value from
getDockedAsideWidth(drawer.left) only when hasLeftAside is true, and similarly
gate rightDockWidth to return a value from getDockedAsideWidth(drawer.right)
only when hasRightAside is true. Otherwise, both should return 0 or a default
value. Apply this same fix to the other locations mentioned (lines 192-193 and
225-226).
---
Outside diff comments:
In `@packages/components/src/layout/composables/useLayoutRootState.ts`:
- Around line 113-121: The resolvedFloatingState is not mode-guarded in the
resolvedFloating computed property, causing cached floating state to persist
when the mode switches from floating to normal. Gate the nextFloatingState
assignment by checking the mode first, similar to how nextFloatingOptions is
already guarded - only retrieve floatingState.resolvedState.value when
props.mode === 'floating', otherwise set nextFloatingState to undefined to
prevent stale state from remaining after a mode switch.
In `@packages/components/src/layout/utils/cssLength.ts`:
- Around line 6-21: The ZERO_LENGTH_RE regex pattern at the top of the
resolveCssLengthToPx function currently only matches plain zero values like "0"
or "0.0" but does not match zero-valued CSS lengths with units such as "0rem",
"0%", or "0px". When these unit-based values are tested against the regex in the
function, the test fails and the function falls through to other logic instead
of correctly returning 0. Update the ZERO_LENGTH_RE regex pattern to also match
optional CSS units (such as rem, px, %, em, vh, vw, etc.) that may appear after
the zero value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 6a8038d9-3cb2-4ff5-ad57-2c28c7897bc1
📒 Files selected for processing (20)
packages/components/src/layout/Layout.vuepackages/components/src/layout/LayoutAsideToggle.vuepackages/components/src/layout/LayoutProxyScrollbar.vuepackages/components/src/layout/components/AsideContent.vuepackages/components/src/layout/components/AsideResizeTrigger.vuepackages/components/src/layout/components/FloatingDragBar.vuepackages/components/src/layout/components/FloatingResizeTriggers.vuepackages/components/src/layout/components/LayoutSurface.vuepackages/components/src/layout/composables/useLayoutContext.tspackages/components/src/layout/composables/useLayoutRootState.tspackages/components/src/layout/composables/usePointerDragSession.tspackages/components/src/layout/index.type.tspackages/components/src/layout/internal.type.tspackages/components/src/layout/utils/asideEventEmitters.tspackages/components/src/layout/utils/asidePresets.tspackages/components/src/layout/utils/cssLength.tspackages/components/src/layout/utils/layoutElements.tspackages/components/src/layout/utils/number.tspackages/components/src/layout/utils/slots.tspackages/components/src/layout/utils/surfaceGeometry.ts
💤 Files with no reviewable changes (1)
- packages/components/src/layout/utils/number.ts
✅ Files skipped from review due to trivial changes (2)
- packages/components/src/layout/utils/asidePresets.ts
- packages/components/src/layout/utils/layoutElements.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/components/src/layout/LayoutAsideToggle.vue
- packages/components/src/layout/utils/surfaceGeometry.ts

背景
文档在线预览 🔗
新增
Layout布局组件,统一承载页面骨架、可收起侧栏、浮层工作区和主区代理滚动条这几类布局能力。这个组件的目标不是做一个简单的页面容器,而是提供一套可组合、可控的布局基础能力,覆盖以下几类常见场景:
header / main / footer / left aside / right asidedock/drawer组件能力
1. 标准布局骨架
Layout提供以下基础插槽:left-asideheadermainfooterright-aside内部使用 grid 组织整体结构。普通模式下参与正常文档流;浮层模式下通过
Teleport挂载到body,并支持拖拽与缩放。2. 双侧栏模型
左右侧栏都支持统一配置:
modeopen/defaultOpenexpandedWidth/defaultExpandedWidthminExpandedWidth/maxExpandedWidthcollapsedWidthcollapseEffectresizable支持两种展示模式:
dock:占据布局空间drawer:覆盖在主区之上支持两种收起表现:
overlay:内容区保持原位slide:内容区跟随侧栏宽度变化当
collapsedWidth > 0时,侧栏关闭后进入 rail 状态;否则进入完全隐藏状态。3. 受控 / 非受控状态
Layout的侧栏与浮层状态都同时支持:侧栏:
open/defaultOpenexpandedWidth/defaultExpandedWidth浮层:
floatingState/defaultFloatingState当前实现中,状态收口在
useControllableState,default*只用于非受控初始化,受控判定统一基于显式 prop 是否为undefined。设计说明
一、状态源集中在 root state
useLayoutRootState负责管理布局的原始状态与受控/非受控同步,包括:这一层只负责状态解析和状态提交,不负责模板结构。
二、结构编排回收到
Layout.vue这一版没有继续把渲染层和 drawer 行为拆成额外 composable,而是把结构编排保留在
Layout.vue:原因是这些规则只服务于
Layout自身,保留在根组件里更直接,也更符合当前场景。三、浮层交互拆成“结构组件 + 子交互组件”
浮层相关能力拆为:
LayoutSurface:负责浮层容器、定位、状态同步与交互编排FloatingDragBar:负责拖拽入口FloatingResizeTriggers:负责缩放入口其中 drag / resize 在输入层面已经解耦,
LayoutSurface只负责统一编排当前交互状态、提交位置尺寸变化,并向外发出浮层事件。四、侧栏 resize 采用事件上抛
侧栏宽度拖拽链路为:
AsideResizeTrigger负责指针交互与宽度计算AsideContent负责向父层透传事件Layout负责承接宽度变化并更新 panel 状态Layout再向外发出公共 resize 事件父层只维护一份
isAsideResizing过程态,用于禁用过渡和切换交互样式,避免父子之间重复维护 resizing 状态。五、
Layout.AsideToggle通过内部 context 共享最小状态Layout.AsideToggle通过provide/inject获取内部上下文,但上下文只暴露最小必要能力:isOpentoggle它不直接暴露完整 panel 状态,也不允许插槽内容拿到一整套可写控制器。这样可以保持数据流单向:
Layout向下传递六、双层结构是有意设计
Layout采用:tr-layouttr-layout__body根层负责:
内容层负责:
这样可以同时满足“浮层外沿交互层需要
overflow: visible”和“内容区需要统一裁切”这两类需求。实现细节
1. 侧栏背景变量语义化
侧栏背景使用显式变量:
--tr-layout-left-aside-bg--tr-layout-right-aside-bg和
header / main / footer的背景变量保持一致的命名粒度。2. drawer 宽度支持显式变量覆盖
drawer宽度优先由--tr-layout-drawer-width控制;未设置时回退到侧栏展开宽度。3. floating resize handle 收敛为 7 个方向
当前浮层缩放保留 7 个 handle:
sewnenwsesw顶部中间
nhandle 被移除,避免与顶部拖拽入口冲突。4. 代理滚动条采用“滚动宿主外置”模型
Layout.ProxyScrollbar不直接决定主区谁来滚动,而是通过scrollTarget接收真实滚动宿主:ProxyScrollbar负责滚动条显示、拖拽和同步这种方式比组件内部隐式接管滚动宿主样式更稳,也更符合显式契约。
对外 API
Props
Layout:modeleftAsiderightAsidefloatingStatedefaultFloatingStatefloatingOptionsLayout.ProxyScrollbar:scrollTargetLayout.AsideToggle:sideEvents
侧栏事件:
aside-open-changeleft-aside-open-changeright-aside-open-changeaside-resize-startaside-resizeaside-resize-endleft-aside-resize-startleft-aside-resizeleft-aside-resize-endright-aside-resize-startright-aside-resizeright-aside-resize-end浮层事件:
update:floatingStatefloating-drag-startfloating-dragfloating-drag-endfloating-resize-startfloating-resizefloating-resize-endSlots
Layout:left-asideheadermainfooterright-asideLayout.AsideToggle:default其中:
left-aside/right-aside作为纯内容插槽使用Layout.AsideToggle的默认插槽{ isOpen }获取组件结构
核心文件如下:
Layout.vue:布局根组件,负责结构编排、drawer 行为和事件桥接LayoutAsideToggle.vue:侧栏开关组件LayoutProxyScrollbar.vue:主区代理滚动条组件LayoutSurface.vue:浮层外壳与浮层交互编排AsideContent.vue:侧栏结构包装AsideResizeTrigger.vue:侧栏拖宽触发器FloatingDragBar.vue:浮层拖拽入口FloatingResizeTriggers.vue:浮层缩放入口useLayoutRootState.ts:原始状态与受控/非受控同步useLayoutContext.ts:provide/inject 上下文usePointerDragSession.ts:指针拖拽会话抽象验证
pnpm.cmd -F @opentiny/tiny-robot buildSummary by CodeRabbit
New Features
Styling