Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -378,6 +378,8 @@ components/
<ToastContainer />
```

`Modal` 不传 `title` / `header` 时会保留右上角关闭按钮,但不额外渲染整条头部。需要调整圆角时可传 `radius="1.4rem"`,内部下拉等浮层需要越界显示时可传 `bodyOverflow="visible"`。

### 声明式 CRUD 页面(CrudPage)

```svelte
Expand Down
50 changes: 44 additions & 6 deletions packages/ui/src/lib/components/compounds/Modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
open = $bindable(false),
title = '',
size = 'md',
radius = '1rem',
closeOnBackdrop = true,
closeOnEscape = true,
showClose = true,
Expand All @@ -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<string, string> = {
'xs': 'hai-modal__panel--xs',
'sm': 'hai-modal__panel--sm',
Expand All @@ -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,
Expand Down Expand Up @@ -138,6 +150,25 @@
onkeydown={handleViewportKeydown}
>
<div class={modalBoxClass} style={panelStyle}>
{#if hasFloatingClose}
<button
type='button'
class='hai-modal__close hai-modal__close--floating'
aria-label={uiM('common_close')}
onclick={handleClose}
>
<svg viewBox='0 0 24 24' class='hai-modal__close-icon' aria-hidden='true'>
<path
d='M6 6l12 12M18 6L6 18'
fill='none'
stroke='currentColor'
stroke-width='1.9'
stroke-linecap='round'
></path>
</svg>
</button>
{/if}

{#if hasHeader}
<!-- 顶部栏单独固定,保证滚动时标题和关闭操作始终可见。 -->
<div class='hai-modal__header flex flex-none items-start justify-between gap-4 px-6 py-5 sm:px-7'>
Expand Down Expand Up @@ -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:
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
}
</style>
41 changes: 41 additions & 0 deletions packages/ui/src/lib/components/scenes/app/ThemeColorPicker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,44 @@
/>
</div>
</div>

<style>
button {
border-color: var(--soft-border, color-mix(in oklab, var(--color-base-content) 12%, transparent));
background: var(--soft-surface, var(--color-base-100));
color: var(--soft-text-body, var(--color-base-content));
box-shadow: none;
font-weight: 500;
}

button:hover:not(:disabled) {
border-color: color-mix(in oklab, var(--color-primary) 28%, var(--soft-border, transparent) 72%);
background: color-mix(
in oklab,
var(--soft-surface, var(--color-base-100)) 88%,
var(--color-base-200) 12%
);
}

button[aria-pressed='true'] {
border-color: color-mix(in oklab, var(--color-primary) 42%, var(--soft-border, transparent) 58%);
background: color-mix(
in oklab,
var(--color-primary) 10%,
var(--soft-surface, var(--color-base-100)) 90%
);
color: var(--color-primary);
}

button:focus,
button:focus-visible {
outline: none;
border-color: color-mix(in oklab, var(--color-primary) 48%, var(--soft-border, transparent) 52%);
box-shadow: 0 0 0 4px color-mix(in oklab, var(--color-primary) 12%, transparent 88%);
}

button > span:first-child {
border-color: color-mix(in oklab, var(--color-base-content) 10%, transparent 90%);
box-shadow: none;
}
</style>
2 changes: 2 additions & 0 deletions packages/ui/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,8 @@ export interface ModalProps {
title?: string
/** 尺寸 */
size?: Size | 'full'
/** 面板圆角(任意合法 CSS border-radius 值,默认 1rem) */
radius?: string
/** 是否可通过点击遮罩关闭 */
closeOnBackdrop?: boolean
/** 是否可通过 ESC 键关闭 */
Expand Down
1 change: 1 addition & 0 deletions packages/ui/tests/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ describe('组合组件 Props', () => {
expectTypeOf<ModalProps>().toHaveProperty('open')
expectTypeOf<ModalProps>().toHaveProperty('title')
expectTypeOf<ModalProps>().toHaveProperty('size')
expectTypeOf<ModalProps>().toHaveProperty('radius')
expectTypeOf<ModalProps>().toHaveProperty('closeOnBackdrop')
expectTypeOf<ModalProps>().toHaveProperty('width')
expectTypeOf<ModalProps>().toHaveProperty('height')
Expand Down
Loading