feat(layout): add layout component#364
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe PR adds a full layout component system: new public and internal types, state and pointer helpers, floating and aside interaction components, a proxy scrollbar, root layout assembly, and package/style exports. ChangesLayout component system
Estimated code review effort🎯 5 (Critical) | ⏱️ ~90+ minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ 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@826207a pnpm add https://pkg.pr.new/@opentiny/tiny-robot-kit@826207a pnpm add https://pkg.pr.new/@opentiny/tiny-robot-svgs@826207a commit: 826207a |
…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
… 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
…aside panel states
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/components/src/layout/Layout.vue (1)
62-70: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRestore keyboard dismissal for drawer overlays.
The backdrop only closes on pointer input. When a drawer is open, Escape should also call
closeDrawers()so keyboard users have a reliable dismiss path.Also applies to: 224-224
🤖 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/Layout.vue` around lines 62 - 70, Restore keyboard dismissal for drawer overlays by wiring Escape to the existing close path in Layout.vue. Update the drawer overlay handling so pressing Escape invokes closeDrawers() for open drawers, using the existing closeDrawers, leftPanel, and rightPanel logic rather than adding a separate close flow. Ensure the keyboard handler is attached where drawer overlay events are managed so keyboard users can dismiss both drawers reliably.
🤖 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/components/FloatingDragBar.vue`:
- Around line 30-45: The dragging state in FloatingDragBar.vue is initialized
from useDraggable’s initialValue only once, so it can drift from later prop
updates. Update the FloatingDragBar setup to keep the draggable position in sync
with props.x and props.y by using the position ref returned from useDraggable
and watching those props; when the bar is not actively dragging, assign the
current prop values back into position so the drag anchor stays aligned after
external moves or resizes.
In `@packages/components/src/layout/composables/useLayoutAsideStates.ts`:
- Around line 14-16: The width resolution helper in resolveFiniteNumber still
allows negative numbers to pass through, so collapsedWidth, minExpandedWidth,
maxExpandedWidth, and expandedWidth can end up negative in layout state. Update
resolveFiniteNumber in useLayoutAsideStates to return the fallback for
undefined, non-finite, or any value below zero, so the computed layout CSS
variables and resize bounds are always non-negative.
In `@packages/components/src/layout/Layout.vue`:
- Around line 36-42: The drawer toggle logic in setDrawerOpen and closeDrawers
can emit close events for a sibling drawer even when that aside slot is not
rendered. Update the Layout.vue drawer helpers so they only call
sibling.setOpen(false) when the corresponding aside is actually present, using
hasLeftAside.value and hasRightAside.value to gate the left/right drawer paths,
and keep the existing panel.setOpen(nextOpen) behavior unchanged.
- Around line 23-28: The `rootEl` computed in `Layout.vue` is resolving
`surfaceRef` with `unrefElement`, but `LayoutSurface` is a teleported component
so the component instance does not expose the actual inner DOM node. Update
`LayoutSurface.vue` to create and expose a `surfaceEl` ref via the component’s
expose contract (`LayoutSurfaceExpose`), then change `rootEl` in `Layout.vue` to
read that exposed `surfaceEl` instead of relying on `unrefElement(surfaceRef)`
so consumers get the real layout surface element.
---
Outside diff comments:
In `@packages/components/src/layout/Layout.vue`:
- Around line 62-70: Restore keyboard dismissal for drawer overlays by wiring
Escape to the existing close path in Layout.vue. Update the drawer overlay
handling so pressing Escape invokes closeDrawers() for open drawers, using the
existing closeDrawers, leftPanel, and rightPanel logic rather than adding a
separate close flow. Ensure the keyboard handler is attached where drawer
overlay events are managed so keyboard users can dismiss both drawers reliably.
🪄 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: bddd32c9-f04a-44b2-b49c-6d029567f93b
📒 Files selected for processing (16)
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/FloatingResizeTrigger.vuepackages/components/src/layout/components/FloatingResizeTriggers.vuepackages/components/src/layout/components/LayoutSurface.vuepackages/components/src/layout/composables/useLayoutAsideStates.tspackages/components/src/layout/composables/useLayoutContext.tspackages/components/src/layout/composables/usePointerDrag.tspackages/components/src/layout/index.type.tspackages/components/src/layout/internal.type.tspackages/components/src/layout/utils/surfaceGeometry.tspackages/components/src/shared/composables/useControllableState.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/components/src/layout/LayoutAsideToggle.vue
- packages/components/src/layout/components/AsideResizeTrigger.vue
- packages/components/src/layout/index.type.ts
|
@coderabbitai review latest commit |
|
✅ Action performedReview finished.
|
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 (1)
packages/components/src/layout/components/AsideResizeTrigger.vue (1)
103-114: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDon't let
minWidthoverride the geometry cap.When the layout is narrower than
props.minWidth + props.oppositeDockWidth + --tr-layout-main-min-width,effectiveMaxis forced back up toprops.minWidth, so dragging can still squeeze the main area below its configured minimum. Cap the upper bound to the actual available width and handle theavailable < minWidthcase explicitly.Suggested fix
function resolveResizeBounds(layoutEl: HTMLElement, asideEl: HTMLElement): ResizeBounds { const rootRect = layoutEl.getBoundingClientRect() const mainMinWidthValue = getComputedStyle(layoutEl).getPropertyValue('--tr-layout-main-min-width').trim() const mainMinWidth = resolveCssLengthToPx(mainMinWidthValue, 320) const startWidth = asideEl.getBoundingClientRect().width - const maxAvailableWidth = rootRect.width - mainMinWidth - props.oppositeDockWidth + const maxAvailableWidth = Math.max(0, rootRect.width - mainMinWidth - props.oppositeDockWidth) + const effectiveMax = Math.min(props.maxWidth, maxAvailableWidth) + const minWidth = Math.min(props.minWidth, effectiveMax) return { startWidth, - minWidth: props.minWidth, - effectiveMax: Math.max(props.minWidth, Math.min(props.maxWidth, maxAvailableWidth)), + minWidth, + effectiveMax, } }🤖 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/components/AsideResizeTrigger.vue` around lines 103 - 114, In resolveResizeBounds, the current effectiveMax calculation lets props.minWidth override the actual geometry limit, which can still shrink the main area too far. Update the ResizeBounds logic in AsideResizeTrigger.vue so the upper bound is capped by the real available width first, then handle the case where maxAvailableWidth is below props.minWidth explicitly instead of forcing it back up. Keep the fix localized to resolveResizeBounds and preserve the existing minWidth/maxWidth behavior where the layout can actually support it.
♻️ Duplicate comments (1)
packages/components/src/layout/composables/usePointerDrag.ts (1)
52-53: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winOnly treat explicit cancellation sentinels as aborted starts.
Tcan legally be0or'', but!contextdrops those valid drag contexts.Proposed fix
- if (!context) { + if (context === false || context === null || context === undefined) { return }🤖 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/usePointerDrag.ts` around lines 52 - 53, The early return in usePointerDrag is treating any falsy drag context as missing, which incorrectly rejects valid values like 0 or ''. Update the start/cancel check to only abort when the context is the explicit cancellation sentinel (for example a null/undefined-style value used by the drag flow), and keep valid falsy contexts flowing through the drag start logic in usePointerDrag.
🤖 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/composables/usePointerDrag.ts`:
- Around line 61-66: In usePointerDrag, the setPointerCapture error path
currently calls finishDrag('manual') and then rethrows, which turns a
recoverable pointer-capture failure into an uncaught handler error. Update the
try/catch around handleEl.setPointerCapture in usePointerDrag so the catch only
performs the cleanup via finishDrag('manual') and does not rethrow the caught
error.
---
Outside diff comments:
In `@packages/components/src/layout/components/AsideResizeTrigger.vue`:
- Around line 103-114: In resolveResizeBounds, the current effectiveMax
calculation lets props.minWidth override the actual geometry limit, which can
still shrink the main area too far. Update the ResizeBounds logic in
AsideResizeTrigger.vue so the upper bound is capped by the real available width
first, then handle the case where maxAvailableWidth is below props.minWidth
explicitly instead of forcing it back up. Keep the fix localized to
resolveResizeBounds and preserve the existing minWidth/maxWidth behavior where
the layout can actually support it.
---
Duplicate comments:
In `@packages/components/src/layout/composables/usePointerDrag.ts`:
- Around line 52-53: The early return in usePointerDrag is treating any falsy
drag context as missing, which incorrectly rejects valid values like 0 or ''.
Update the start/cancel check to only abort when the context is the explicit
cancellation sentinel (for example a null/undefined-style value used by the drag
flow), and keep valid falsy contexts flowing through the drag start logic in
usePointerDrag.
🪄 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: c318a503-2c3b-4904-be3f-70f7a2825567
📒 Files selected for processing (11)
packages/components/src/layout/Layout.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/components/LayoutSurface.vuepackages/components/src/layout/composables/useLayoutAsideStates.tspackages/components/src/layout/composables/usePointerDrag.tspackages/components/src/layout/internal.type.tspackages/components/src/layout/utils/asideEventEmitters.tspackages/components/src/layout/utils/domInteraction.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/components/src/layout/components/FloatingResizeTrigger.vue
- packages/components/src/layout/Layout.vue
- packages/components/src/layout/components/LayoutSurface.vue
- packages/components/src/layout/LayoutProxyScrollbar.vue
…n scroll target changes
🧹 Preview Cleaned UpThe preview deployment has been removed. |

背景
文档在线预览 🔗
新增
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
Layoutcomponent with left/right aside panels (dock/drawer/rail) including open/close toggles and width resize.LayoutProxyScrollbarfor rendering a draggable proxy scrollbar tied to a scroll target.