diff --git a/packages/ui/README.md b/packages/ui/README.md index 3fc5d111..ccddb48d 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -151,7 +151,7 @@ components/ | 组件 | 描述 | 主要属性 | | --------- | ------ | ------------------------------------------------------- | -| `Modal` | 模态框 | `open`, `title`, `size`, `closeOnBackdrop`, `showClose` | +| `Modal` | 模态框 | `open`, `title`, `size`, `radius`, `bodyOverflow`, `showClose` | | `Drawer` | 抽屉 | `open`, `position`, `title`, `size` | | `Confirm` | 确认框 | `open`, `title`, `message`, `variant`, `onconfirm` | | `Popover` | 弹出层 | `open`, `position`, `trigger`, `offset` | @@ -378,6 +378,8 @@ components/ ``` +`Modal` 不传 `title` / `header` 时会保留右上角关闭按钮,但不额外渲染整条头部。需要调整圆角时可传 `radius="1.4rem"`,内部下拉等浮层需要越界显示时可传 `bodyOverflow="visible"`。 + ### 声明式 CRUD 页面(CrudPage) ```svelte diff --git a/packages/ui/src/lib/components/compounds/Modal.svelte b/packages/ui/src/lib/components/compounds/Modal.svelte index 2ddc5729..aa3d7637 100644 --- a/packages/ui/src/lib/components/compounds/Modal.svelte +++ b/packages/ui/src/lib/components/compounds/Modal.svelte @@ -11,6 +11,7 @@ open = $bindable(false), title = '', size = 'md', + radius = '1rem', closeOnBackdrop = true, closeOnEscape = true, showClose = true, @@ -29,8 +30,10 @@ // 只有调用 `showModal()` 进入 top-layer 后,弹框才不会被父级布局和 overflow 裁剪。 let modalElement: HTMLDialogElement | undefined = $state() - // 只要存在标题、自定义 header 或关闭按钮,就渲染顶部栏位,保证弹框结构稳定。 - const hasHeader = $derived(Boolean(header || title || showClose)) + // 标题区仅在显式提供标题或自定义 header 时出现; + // 仅需关闭按钮的场景改为右上角悬浮按钮,避免业务侧额外占一整行头部。 + const hasHeader = $derived(Boolean(header || title)) + const hasFloatingClose = $derived(showClose && !hasHeader) const sizeMap: Record = { 'xs': 'hai-modal__panel--xs', 'sm': 'hai-modal__panel--sm', @@ -55,12 +58,21 @@ styles.push(`--hai-modal-height: ${height.trim()}`) } + if (typeof radius === 'string' && radius.trim().length > 0) { + styles.push(`--hai-modal-radius: ${radius.trim()}`) + } + + if (bodyOverflow === 'visible') { + styles.push('--hai-modal-panel-overflow: visible') + } + return styles.length > 0 ? styles.join('; ') : undefined }) const modalBoxClass = $derived( cn( 'hai-modal__panel flex min-h-0 flex-col', + 'relative', 'bg-base-100 p-0', sizeMap[size], className, @@ -138,6 +150,25 @@ onkeydown={handleViewportKeydown} >
+ {#if hasFloatingClose} + + {/if} + {#if hasHeader}
@@ -225,8 +256,8 @@ height: var(--hai-modal-height, auto); max-height: calc(100vh - clamp(2rem, 5.2vw, 4rem)); max-height: calc(100dvh - clamp(2rem, 5.2vw, 4rem)); - border-radius: 1.4rem; - overflow: hidden; + border-radius: var(--hai-modal-radius, 1rem); + overflow: var(--hai-modal-panel-overflow, hidden); color: var(--color-base-content); outline: none; box-shadow: @@ -247,7 +278,7 @@ border-bottom-right-radius: inherit; border-bottom-left-radius: inherit; box-shadow: inset 0 1px 0 color-mix(in srgb, var(--color-base-content) 6%, transparent); - border-radius: 0 0 1.4rem 1.4rem; + border-radius: 0 0 var(--hai-modal-radius, 1rem) var(--hai-modal-radius, 1rem); } .hai-modal__close { @@ -284,6 +315,13 @@ height: 1rem; } + .hai-modal__close--floating { + position: absolute; + top: 1rem; + right: 1rem; + z-index: 1; + } + .hai-modal__panel--xs { --hai-modal-width: min(100%, 24rem); } @@ -342,7 +380,7 @@ max-width: 100%; max-height: calc(100vh - 1.7rem); max-height: calc(100dvh - 1.7rem); - border-radius: 1rem; + border-radius: min(var(--hai-modal-radius, 1rem), 1rem); } } diff --git a/packages/ui/src/lib/components/scenes/app/ThemeColorPicker.svelte b/packages/ui/src/lib/components/scenes/app/ThemeColorPicker.svelte index 75aae18b..33c14a31 100644 --- a/packages/ui/src/lib/components/scenes/app/ThemeColorPicker.svelte +++ b/packages/ui/src/lib/components/scenes/app/ThemeColorPicker.svelte @@ -163,3 +163,44 @@ />
+ + diff --git a/packages/ui/src/lib/types.ts b/packages/ui/src/lib/types.ts index 9eefa568..28e7f2a2 100644 --- a/packages/ui/src/lib/types.ts +++ b/packages/ui/src/lib/types.ts @@ -432,6 +432,8 @@ export interface ModalProps { title?: string /** 尺寸 */ size?: Size | 'full' + /** 面板圆角(任意合法 CSS border-radius 值,默认 1rem) */ + radius?: string /** 是否可通过点击遮罩关闭 */ closeOnBackdrop?: boolean /** 是否可通过 ESC 键关闭 */ diff --git a/packages/ui/tests/types.test.ts b/packages/ui/tests/types.test.ts index 3f0b2c67..a8166ca5 100644 --- a/packages/ui/tests/types.test.ts +++ b/packages/ui/tests/types.test.ts @@ -217,6 +217,7 @@ describe('组合组件 Props', () => { expectTypeOf().toHaveProperty('open') expectTypeOf().toHaveProperty('title') expectTypeOf().toHaveProperty('size') + expectTypeOf().toHaveProperty('radius') expectTypeOf().toHaveProperty('closeOnBackdrop') expectTypeOf().toHaveProperty('width') expectTypeOf().toHaveProperty('height')