diff --git a/.ua/backlog.json b/.ua/backlog.json index 04d5cec..dc583e2 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -160,6 +160,14 @@ "acceptance": "新增皮肤层 TableColumn extends ColumnDef,追加 align?: left|center|right 与 format?: (value,row)=>string;DataTableProps.columns 改用它;表头与单元格共用对齐类;format 走 textContent 天然免注入;demo 表格演示年龄右对齐+单位、城市居中;manifest/文档/llms-full.txt 同步;npm run check 全绿", "status": "done", "notes": "严格分层:内核 ColumnDef 不动,展示字段只在 skin 的 TableColumn 上扩展(extends 而非改核心);对齐类同时作用于 th(取代原 class 函数,合并 ua-sortable);demo 首表展示 align:right + format(v=>`${v} 岁`)、城市居中" + }, + { + "id": "d14", + "title": "新增抽屉 drawer 组件", + "motivation": "详情查看/编辑需要不打断主视图的侧滑容器,modal 是强打断确认;移动端尤其常用", + "acceptance": "新增 drawer.ts:open 信号驱动,placement 左右、width 可调(窄屏 92vw 上限)、footer 操作区、遮罩 / × / Esc 关闭、打开聚焦首个可交互元素;只消费 --ua-* 令牌 + reduced-motion 兜底;导出 / manifest / 文档 / llms-full.txt / demo 同步;npm run check 全绿", + "status": "done", + "notes": "遮罩开合用 opacity+visibility(与 tooltip/popover 同模式),面板靠 transform:translateX 滑入;width 走 inline style(调用方数据驱动的表现值不进 CSS);demo 新增 sec-drawer(订单详情 + footer 关闭 + tag/badge 组合)" } ] } diff --git a/.ua/index.json b/.ua/index.json index d1e2f29..cc5fe48 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-23T08:55:54.330Z", + "generatedAt": "2026-10-23T08:56:29.143Z", "repoRoot": ".", "packages": [ { @@ -130,7 +130,7 @@ { "name": "@ui-arch/ui", "type": "skin-dom", - "description": "DOM 皮肤组件库:button / textField / textarea / slider / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / tag / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。", + "description": "DOM 皮肤组件库:button / textField / textarea / slider / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / tag / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / drawer / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。", "tags": [ "components", "dom", @@ -166,6 +166,7 @@ "progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色", "tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签", "modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区", + "drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭", "form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit", "formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄", "dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core" @@ -203,7 +204,8 @@ "DataTableProps", "PopoverProps", "SliderProps", - "TagProps" + "TagProps", + "DrawerProps" ] }, "usage": "const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')", @@ -213,7 +215,7 @@ "语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色" ], "docs": { - "intro": "26 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。", + "intro": "27 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。", "steps": [ "安装依赖:pnpm add @ui-arch/ui @ui-arch/frame(workspace 内自动链接)", "引入样式:在应用入口 import '@ui-arch/ui/ui.css',暗色主题在根元素设 data-theme='dark'", @@ -1085,6 +1087,61 @@ "底部操作按钮放进 footer(如取消/确定),需要更宽内容用 size: 'lg',窄确认框用 'sm'" ] }, + { + "id": "drawer", + "title": "抽屉 Drawer", + "demo": "sec-drawer", + "props": [ + { + "n": "title", + "t": "string", + "r": 1, + "d": "标题" + }, + { + "n": "open", + "t": "Signal", + "r": 1, + "d": "开关信号:true 滑出 / false 收起" + }, + { + "n": "placement", + "t": "'left' | 'right'", + "d": "滑出方向;缺省 right", + "df": "right" + }, + { + "n": "width", + "t": "number", + "d": "面板宽度像素(窄屏最多 92vw);缺省 320", + "df": "320" + }, + { + "n": "footer", + "t": "unknown", + "d": "底部操作区内容;缺省不渲染" + }, + { + "n": "onClose", + "t": "() => void", + "k": 1, + "d": "关闭回调(点遮罩 / × / Esc 触发)" + }, + { + "n": "...children", + "t": "unknown[]", + "d": "面板内容子节点" + } + ], + "example": "const open = signal(false); drawer({ title: '详情', open, width: 360, onClose: () => open(false) }, h('p', null, '内容'))", + "steps": [ + "创建布尔信号 open = signal(false),传给 drawer;其余参数为面板内容子节点", + "打开后从侧边滑出:placement: 'right'(默认)或 'left',width 控制宽度(窄屏自动收到 92vw)", + "点遮罩、× 或按 Esc 关闭并写回 false;打开时焦点落在面板首个可交互元素", + "底部操作按钮放进 footer;与 modal 的分工:modal 是强打断的确认,drawer 保留主视图上下文做详情/编辑", + "面板内容与 modal 一样接受任意 DOM 与响应式函数" + ] + }, { "id": "form", "title": "表单 Form / 表单字段 FormItem", diff --git a/.ua/iterations.log b/.ua/iterations.log index 834b3ce..83a21ec 100644 --- a/.ua/iterations.log +++ b/.ua/iterations.log @@ -18,3 +18,4 @@ {"time": "2026-10-23T16:54:36+08:00", "id": "d11", "result": "done", "summary": "modal 新增 size 三档(sm/md/lg)与 footer 操作区(右对齐 + 上分割线),缺省行为不变;demo/manifest/llms 同步,check 全绿"} {"time": "2026-10-23T16:55:10+08:00", "id": "d12", "result": "done", "summary": "新增可关闭标签 tag(五档语义色 + closable/onClose,无状态委托调用方重渲染);demo 用 renderList 演示真实删除;manifest/llms 同步,check 全绿"} {"time": "2026-10-23T16:55:55+08:00", "id": "d13", "result": "done", "summary": "dataTable 列定义新增皮肤层 TableColumn(align + format),内核 ColumnDef 不变;表头/单元格共用对齐类,format 走 textContent;demo/manifest/llms 同步,check 全绿"} +{"time": "2026-10-23T16:56:36+08:00", "id": "d14", "result": "done", "summary": "新增抽屉 drawer:open 信号驱动侧滑、placement/width 可配、footer 操作区、遮罩/×/Esc 关闭并聚焦首元素;demo/manifest/llms 同步,check 全绿"} diff --git a/apps/demo/main.ts b/apps/demo/main.ts index f691d9e..04f0516 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -8,6 +8,7 @@ import { card, checkbox, dataTable, + drawer, form, formItem, menu, @@ -99,6 +100,9 @@ const formResult = signal(''); // —— 分页联动状态:dataTable 与独立 pagination 共用同一页码信号 —— const tablePage = signal(1); +// —— 抽屉开关 —— +const drawerOpen = signal(false); + // —— 标签列表:关闭即写回信号,容器按信号重渲染 —— const tags = signal(['设计', '前端', '组件库']); @@ -164,6 +168,23 @@ const content = h( radioGroup({ options: [{ value: 'x', label: '静态禁用' }], value: size, disabled: true }), ), ), + section( + 'sec-drawer', + '抽屉', + 'drawer:open 信号驱动侧滑面板,保留主视图上下文;遮罩 / × / Esc 关闭。', + button({ label: '打开抽屉', onClick: () => drawerOpen(true) }), + drawer( + { + title: '订单详情', + open: drawerOpen, + width: 360, + onClose: () => drawerOpen(false), + footer: h('div', { class: 'row' }, button({ label: '关闭', variant: 'ghost', onClick: () => drawerOpen(false) })), + }, + h('p', null, '这里是抽屉内容,可放任意 DOM。'), + h('div', { class: 'row' }, tag({ label: '#A1024', variant: 'info' }), badge({ label: '已发货', variant: 'success' })), + ), + ), section( 'sec-tag', '标签', diff --git a/llms-full.txt b/llms-full.txt index 210f5cb..7979dcb 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-23T08:55:54.330Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-23T08:56:29.143Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -40,7 +40,7 @@ ## @ui-arch/ui(skin-dom)v0.1.0 -26 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。 +27 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。 ### button — 按钮 Button @@ -615,6 +615,34 @@ const open = signal(false); modal({ title: '确认操作', open, onClose: () => 现场演示:apps/docs/demo.html#sec-tabs-modal +### drawer — 抽屉 Drawer + +参数: + +- title(必填):string —— 标题 +- open(必填):Signal —— 开关信号:true 滑出 / false 收起 +- placement(默认 right):'left' | 'right' —— 滑出方向;缺省 right +- width(默认 320):number —— 面板宽度像素(窄屏最多 92vw);缺省 320 +- footer:unknown —— 底部操作区内容;缺省不渲染 +- onClose(回调):() => void —— 关闭回调(点遮罩 / × / Esc 触发) +- ...children:unknown[] —— 面板内容子节点 + +示例: + +``` +const open = signal(false); drawer({ title: '详情', open, width: 360, onClose: () => open(false) }, h('p', null, '内容')) +``` + +手把手: + +1. 创建布尔信号 open = signal(false),传给 drawer;其余参数为面板内容子节点 +2. 打开后从侧边滑出:placement: 'right'(默认)或 'left',width 控制宽度(窄屏自动收到 92vw) +3. 点遮罩、× 或按 Esc 关闭并写回 false;打开时焦点落在面板首个可交互元素 +4. 底部操作按钮放进 footer;与 modal 的分工:modal 是强打断的确认,drawer 保留主视图上下文做详情/编辑 +5. 面板内容与 modal 一样接受任意 DOM 与响应式函数 + +现场演示:apps/docs/demo.html#sec-drawer + ### form — 表单 Form / 表单字段 FormItem 参数: @@ -704,6 +732,7 @@ API 速览: - progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色 - tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签 - modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区 +- drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭 - form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit - formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄 - dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core diff --git a/llms.txt b/llms.txt index b91da0b..1940753 100644 --- a/llms.txt +++ b/llms.txt @@ -45,7 +45,7 @@ ## @ui-arch/ui (v0.1.0) - 类型: skin-dom · 目录: packages/skin/ui -- 描述: DOM 皮肤组件库:button / textField / textarea / slider / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / tag / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。 +- 描述: DOM 皮肤组件库:button / textField / textarea / slider / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / tag / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / drawer / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。 - API: - button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用 - textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用 @@ -71,6 +71,7 @@ - progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色 - tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签 - modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区 + - drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭 - form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit - formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄 - dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core diff --git a/packages/skin/ui/src/drawer.ts b/packages/skin/ui/src/drawer.ts new file mode 100644 index 0000000..8f1533b --- /dev/null +++ b/packages/skin/ui/src/drawer.ts @@ -0,0 +1,39 @@ +import { effect, h } from '@ui-arch/frame'; +import type { DrawerProps } from './types.js'; + +/*** 抽屉:open 信号驱动侧滑面板,点遮罩 / × / Esc 关闭;与 modal 的分工是不打断主视图、可从侧边保留上下文 ***/ +export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement { + const close = (): void => { + if (!props.open()) return; // 卫语句:已关闭不重复触发回调 + props.open(false); + props.onClose?.(); + }; + const overlay = h('div', { + class: () => `ua-drawer-overlay${props.open() ? ' ua-open' : ''}`, + onClick: (e: Event) => { + if (e.target === overlay) close(); // 卫语句:仅点遮罩本身关闭 + }, + }); + const panel = h( + 'aside', + { + class: `ua-drawer ua-drawer-${props.placement ?? 'right'}`, + style: `width:${props.width ?? 320}px`, + role: 'dialog', + 'aria-hidden': () => String(!props.open()), + }, + h('div', { class: 'ua-drawer-head' }, h('h3', { class: 'ua-drawer-title' }, props.title), h('button', { class: 'ua-drawer-close', 'aria-label': '关闭抽屉', onClick: close }, '×')), + h('div', { class: 'ua-drawer-body' }, ...children), + props.footer ? h('div', { class: 'ua-drawer-foot' }, props.footer) : null, + ); + overlay.append(panel); + + effect(() => { + if (!props.open()) return; + (panel.querySelector('button, input, textarea') as HTMLElement | null)?.focus(); // 打开时聚焦首个可交互元素 + }); + document.addEventListener('keydown', (e: KeyboardEvent) => { + if (e.key === 'Escape' && props.open()) close(); + }); + return overlay; +} diff --git a/packages/skin/ui/src/index.ts b/packages/skin/ui/src/index.ts index 4735dbe..2300cfa 100644 --- a/packages/skin/ui/src/index.ts +++ b/packages/skin/ui/src/index.ts @@ -23,5 +23,6 @@ export { spinner } from './spinner.js'; export { progress } from './progress.js'; export { tabs } from './tabs.js'; export { modal } from './modal.js'; +export { drawer } from './drawer.js'; export { form, formItem } from './form.js'; export { dataTable } from './dataTable.js'; diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 864b91f..da31f69 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -232,6 +232,16 @@ export interface ModalProps { onClose?: () => void; // 关闭回调(点遮罩、× 或 Esc 触发) } +/*** 抽屉入参 ***/ +export interface DrawerProps { + title: string; // 标题 + open: Signal; // 开关信号:true 滑出 / false 收起 + placement?: 'left' | 'right'; // 滑出方向;缺省 right + width?: number; // 面板宽度像素;缺省 320 + footer?: unknown; // 底部操作区内容;缺省不渲染 + onClose?: () => void; // 关闭回调(遮罩 / × / Esc 触发) +} + /*** 表单校验规则:按序执行,首个失败即停止 ***/ export interface FormRule { required?: boolean; // 非空校验(trim 后判定) diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 7c45d0f..489ca7d 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -1,7 +1,7 @@ { "name": "@ui-arch/ui", "type": "skin-dom", - "description": "DOM 皮肤组件库:button / textField / textarea / slider / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / tag / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。", + "description": "DOM 皮肤组件库:button / textField / textarea / slider / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / tag / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / drawer / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。", "tags": [ "components", "dom", @@ -37,6 +37,7 @@ "progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色", "tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签", "modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区", + "drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭", "form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit", "formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄", "dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core" @@ -74,7 +75,8 @@ "DataTableProps", "PopoverProps", "SliderProps", - "TagProps" + "TagProps", + "DrawerProps" ] }, "usage": "const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')", @@ -84,7 +86,7 @@ "语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色" ], "docs": { - "intro": "26 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。", + "intro": "27 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。", "steps": [ "安装依赖:pnpm add @ui-arch/ui @ui-arch/frame(workspace 内自动链接)", "引入样式:在应用入口 import '@ui-arch/ui/ui.css',暗色主题在根元素设 data-theme='dark'", @@ -956,6 +958,61 @@ "底部操作按钮放进 footer(如取消/确定),需要更宽内容用 size: 'lg',窄确认框用 'sm'" ] }, + { + "id": "drawer", + "title": "抽屉 Drawer", + "demo": "sec-drawer", + "props": [ + { + "n": "title", + "t": "string", + "r": 1, + "d": "标题" + }, + { + "n": "open", + "t": "Signal", + "r": 1, + "d": "开关信号:true 滑出 / false 收起" + }, + { + "n": "placement", + "t": "'left' | 'right'", + "d": "滑出方向;缺省 right", + "df": "right" + }, + { + "n": "width", + "t": "number", + "d": "面板宽度像素(窄屏最多 92vw);缺省 320", + "df": "320" + }, + { + "n": "footer", + "t": "unknown", + "d": "底部操作区内容;缺省不渲染" + }, + { + "n": "onClose", + "t": "() => void", + "k": 1, + "d": "关闭回调(点遮罩 / × / Esc 触发)" + }, + { + "n": "...children", + "t": "unknown[]", + "d": "面板内容子节点" + } + ], + "example": "const open = signal(false); drawer({ title: '详情', open, width: 360, onClose: () => open(false) }, h('p', null, '内容'))", + "steps": [ + "创建布尔信号 open = signal(false),传给 drawer;其余参数为面板内容子节点", + "打开后从侧边滑出:placement: 'right'(默认)或 'left',width 控制宽度(窄屏自动收到 92vw)", + "点遮罩、× 或按 Esc 关闭并写回 false;打开时焦点落在面板首个可交互元素", + "底部操作按钮放进 footer;与 modal 的分工:modal 是强打断的确认,drawer 保留主视图上下文做详情/编辑", + "面板内容与 modal 一样接受任意 DOM 与响应式函数" + ] + }, { "id": "form", "title": "表单 Form / 表单字段 FormItem", diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 9274749..a372aa5 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -1014,6 +1014,82 @@ margin-top: 4px; } +/* —— 抽屉(侧滑面板) —— */ +.ua-drawer-overlay { + position: fixed; + inset: 0; + z-index: 90; + background: rgba(0, 0, 0, 0.36); + opacity: 0; + visibility: hidden; + transition: opacity 0.22s ease, visibility 0.22s; +} +.ua-drawer-overlay.ua-open { + opacity: 1; + visibility: visible; +} +.ua-drawer { + position: absolute; + top: 0; + bottom: 0; + display: flex; + flex-direction: column; + max-width: 92vw; + background: var(--ua-color-surface); + border-left: 1px solid var(--ua-color-border); + box-shadow: 0 0 40px rgba(0, 0, 0, 0.22); + transition: transform 0.24s ease; +} +.ua-drawer-right { + right: 0; + transform: translateX(100%); +} +.ua-drawer-left { + left: 0; + border-left: none; + border-right: 1px solid var(--ua-color-border); + transform: translateX(-100%); +} +.ua-drawer-overlay.ua-open .ua-drawer-right, +.ua-drawer-overlay.ua-open .ua-drawer-left { + transform: none; +} +.ua-drawer-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--ua-space-2); + padding: 12px 16px; + border-bottom: 1px solid var(--ua-color-border); +} +.ua-drawer-title { + margin: 0; + font-size: var(--ua-font-size-lg); + font-weight: 500; +} +.ua-drawer-close { + border: none; + background: none; + color: var(--ua-color-text-muted); + font-size: 18px; + line-height: 1; + cursor: pointer; +} +.ua-drawer-body { + flex: 1; + overflow-y: auto; + padding: 16px; + color: var(--ua-color-text); + font-size: var(--ua-font-size-md); +} +.ua-drawer-foot { + display: flex; + justify-content: flex-end; + gap: var(--ua-space-2); + padding: 12px 16px; + border-top: 1px solid var(--ua-color-border); +} + /* —— 弹窗 —— */ .ua-modal-overlay { display: none; @@ -1182,7 +1258,9 @@ .ua-skeleton::after, .ua-form-error, .ua-modal, - .ua-modal-overlay { + .ua-modal-overlay, + .ua-drawer, + .ua-drawer-overlay { animation: none !important; transition: none !important; }