From 8b5f355d7e7298237c90789df58320311f807a26 Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 11:46:54 +0800 Subject: [PATCH] =?UTF-8?q?iter(g35):=20=E6=96=B0=E5=A2=9E=20notification?= =?UTF-8?q?=20=E9=80=9A=E7=9F=A5=E5=8D=A1=E7=89=87=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/backlog.json | 8 +++ .ua/index.json | 51 +++++++++++++++++- AGENTS.md | 2 +- ARCHITECTURE.md | 2 +- README.md | 6 +-- apps/demo/main.ts | 14 +++++ llms-full.txt | 30 ++++++++++- llms.txt | 1 + packages/skin/ui/src/index.ts | 1 + packages/skin/ui/src/notification.ts | 56 +++++++++++++++++++ packages/skin/ui/src/types.ts | 10 ++++ packages/skin/ui/ua.manifest.json | 49 ++++++++++++++++- packages/skin/ui/ui.css | 81 ++++++++++++++++++++++++++++ 13 files changed, 302 insertions(+), 9 deletions(-) create mode 100644 packages/skin/ui/src/notification.ts diff --git a/.ua/backlog.json b/.ua/backlog.json index 7b489b0..ceb299a 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -636,6 +636,14 @@ "acceptance": "ua info <组件id> 输出 props 表 + 步骤 + 示例,ua info <包名> 出摘要且 --json 保留原始清单;MCP get_component 支持组件 id(返回 package + component);npm run check 全绿", "status": "done", "notes": "自补:Agent 查询体验;--json 保留原始清单" + }, + { + "id": "g35", + "title": "新增 notification 通知卡片", + "motivation": "toast 是右上角轻量瞬时提示,装不下「标题 + 描述 + 操作按钮 + 需读完」的通知场景,业务侧只能自己写浮层", + "acceptance": "notification({ title, description, variant, duration, closable, action }) 右下角堆叠(与 toast 分区)、超出 3 条挤掉最早、返回 { el, close }、duration 0 常驻;组件数 42→43 三处计数同步;npm run check 全绿", + "status": "open", + "notes": "自补:组件补充" } ] } diff --git a/.ua/index.json b/.ua/index.json index 8d07d32..f29dab7 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T03:45:25.126Z", + "generatedAt": "2026-10-26T03:46:32.583Z", "repoRoot": ".", "packages": [ { @@ -185,7 +185,8 @@ "list(props):列表,header/footer 外壳 + 条目 avatar/title/description/extra,onItemClick 让条目可点击,items 为空回落空状态", "carousel(props):轮播,items 横排位移,指示点/箭头/←→ 键切换,autoplay 毫秒自动前进且悬停暂停,index 信号可外部受控", "tree(props):树形,递归节点 + 箭头展开/收起 + 点选节点,键盘 Enter 选中、→ 展开、← 收起,禁用节点不响应", - "upload(props):文件选择,点击或拖拽入列,清单可逐个移除,onChange 回传文件数组(不内置上传请求)" + "upload(props):文件选择,点击或拖拽入列,清单可逐个移除,onChange 回传文件数组(不内置上传请求)", + "notification(props):右下角通知卡片(标题 + 描述 + 操作区),返回 { el, close } 句柄,缺省常驻需手动关闭" ], "keyTypes": [ "ButtonProps", @@ -1946,6 +1947,52 @@ "键盘:区域可 Tab 聚焦,Enter/空格 打开选择器;拖拽时区域高亮,拖出恢复", "disabled 时不可点选、不接受拖拽;已选清单的 × 会触发 onChange 回传剩余文件" ] + }, + { + "id": "notification", + "title": "通知卡片 Notification", + "demo": "sec-notification", + "props": [ + { + "n": "title", + "t": "string", + "r": 1, + "d": "通知标题" + }, + { + "n": "description", + "t": "string", + "d": "补充说明;缺省不渲染" + }, + { + "n": "variant", + "t": "'info' | 'success' | 'warning' | 'danger'", + "d": "语义色(左侧条与圆点);缺省 info" + }, + { + "n": "duration", + "t": "number", + "d": "自动关闭毫秒;缺省 0 = 常驻不自动关" + }, + { + "n": "closable", + "t": "boolean", + "d": "是否显示关闭按钮;缺省 true" + }, + { + "n": "action", + "t": "unknown", + "d": "操作区内容(如按钮);缺省不渲染" + } + ], + "example": "const { close } = notification({ title: '导出完成', description: 'report.csv 已开始下载', variant: 'success', duration: 4000 }); // close() 手动关闭", + "steps": [ + "调用即入列:右下角堆叠,同屏最多 3 条,超出自动挤掉最早一条", + "缺省常驻(duration 0),需要自动消失就传毫秒数,如 duration: 4000", + "返回 { el, close } 句柄:程序化关闭用 close(),需要自己挂交互就用 el", + "action 放操作区(常见为按钮);variant 决定左侧条与圆点语义色", + "与 toast 的分工:toast 右上角轻量瞬时,notification 右下角可长读、可操作" + ] } ] }, diff --git a/AGENTS.md b/AGENTS.md index 2291f18..3ebe551 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ - `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核 - `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架 -- `packages/skin/ui`(@ui-arch/ui):42 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌 +- `packages/skin/ui`(@ui-arch/ui):43 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌 - `packages/design/tokens`(@ui-arch/tokens):设计令牌 - `packages/tools/cli`(@ui-arch/cli):`ua` 命令行 + MCP 注册表服务(private) - `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f09f6c9..578e583 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -34,7 +34,7 @@ ui-arch-mvp/ │ │ ├── table-core/ # 表格状态内核 │ │ └── frame/ # 信号 + hyperscript 微框架 │ ├── skin/ # 皮肤层(DOM 组件,可发布) -│ │ └── ui/ # 42 个组件 + ui.css +│ │ └── ui/ # 43 个组件 + ui.css │ ├── design/ # 资产层(可发布) │ │ └── tokens/ # --ua-* 设计令牌 │ └── tools/ # 工程层(private,不发布) diff --git a/README.md b/README.md index ed216df..445f446 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ npm run demo # 打开演示:apps/demo/dist/demo.html npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全量文档 llms-full.txt ``` -## 组件(共 42 个) +## 组件(共 43 个) -accordion · alert · avatar · badge · breadcrumb · button · carousel · card · checkbox · dataTable · descriptions · divider · drawer · empty · form · image · inputNumber · list · menu · modal · pagination · popover · progress · radioGroup · rate · segmented · select · skeleton · slider · spinner · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip · tree · upload +accordion · alert · avatar · badge · breadcrumb · button · carousel · card · checkbox · dataTable · descriptions · divider · drawer · empty · form · image · inputNumber · list · menu · modal · notification · pagination · popover · progress · radioGroup · rate · segmented · select · skeleton · slider · spinner · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip · tree · upload 分组:表单(button / textField / inputNumber / textarea / checkbox / switchButton / select / radioGroup / slider / rate / segmented / form / formItem)、反馈(alert / toast / spinner / progress / skeleton / badge / empty / statistic)、浮层(tooltip / popover / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper)、数据(dataTable / descriptions / tag / avatar / card)、其他(divider / timeline)。 @@ -25,7 +25,7 @@ accordion · alert · avatar · badge · breadcrumb · button · carousel · car |---|---| | `packages/core/frame` | @ui-arch/frame:signal / computed / effect + hyperscript(无虚拟 DOM) | | `packages/core/table-core` | @ui-arch/table-core:headless 表格(filter → sort → paginate 派生管线) | -| `packages/skin/ui` | @ui-arch/ui:42 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) | +| `packages/skin/ui` | @ui-arch/ui:43 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) | | `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) | | `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server | | `apps/demo` | 交互演示(esbuild 打包成单文件) | diff --git a/apps/demo/main.ts b/apps/demo/main.ts index db7ce4e..3aa9f59 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -28,6 +28,7 @@ import { list, carousel, tree, + notification, upload, menu, modal, @@ -657,6 +658,19 @@ const content = h( ), h('div', { class: 'row' }, upload({ accept: 'image/*', hint: '仅图片(accept: image/*)' }), upload({ disabled: true, hint: '禁用态' })), ), + section( + 'sec-notification', + '通知卡片', + 'notification:右下角堆叠(与右上角 toast 分区),标题 + 描述 + 操作区,缺省常驻需手动关闭;duration 传毫秒可自动消失。', + h( + 'div', + { class: 'row' }, + button({ label: '常驻通知', variant: 'primary', onClick: () => notification({ title: '有 3 条待审报销', description: '王小明 / 李静 / 张伟提交的报销单等待复核', action: button({ label: '去处理', onClick: () => toast.success('已跳转待办') }) }) }), + button({ label: '成功(4s 自动关)', onClick: () => notification({ title: '导出完成', description: 'report-2026-10.csv 已开始下载', variant: 'success', duration: 4000 }) }), + button({ label: '警告(常驻)', variant: 'ghost', onClick: () => notification({ title: '连接不稳定', description: '正在重试第 2 次,请检查网络', variant: 'warning' }) }), + button({ label: '错误(可关)', variant: 'danger', onClick: () => notification({ title: '保存失败', description: '服务端返回 500,请稍后重试', variant: 'danger' }) }), + ), + ), section( 'sec-menu', '下拉菜单', diff --git a/llms-full.txt b/llms-full.txt index 351f4df..579e7a7 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T03:45:25.126Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T03:46:32.583Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -1115,6 +1115,33 @@ upload({ multiple: true, accept: 'image/*', hint: '拖拽图片到此处', onCha 现场演示:apps/docs/demo.html#sec-upload +### notification — 通知卡片 Notification + +参数: + +- title(必填):string —— 通知标题 +- description:string —— 补充说明;缺省不渲染 +- variant:'info' | 'success' | 'warning' | 'danger' —— 语义色(左侧条与圆点);缺省 info +- duration:number —— 自动关闭毫秒;缺省 0 = 常驻不自动关 +- closable:boolean —— 是否显示关闭按钮;缺省 true +- action:unknown —— 操作区内容(如按钮);缺省不渲染 + +示例: + +``` +const { close } = notification({ title: '导出完成', description: 'report.csv 已开始下载', variant: 'success', duration: 4000 }); // close() 手动关闭 +``` + +手把手: + +1. 调用即入列:右下角堆叠,同屏最多 3 条,超出自动挤掉最早一条 +2. 缺省常驻(duration 0),需要自动消失就传毫秒数,如 duration: 4000 +3. 返回 { el, close } 句柄:程序化关闭用 close(),需要自己挂交互就用 el +4. action 放操作区(常见为按钮);variant 决定左侧条与圆点语义色 +5. 与 toast 的分工:toast 右上角轻量瞬时,notification 右下角可长读、可操作 + +现场演示:apps/docs/demo.html#sec-notification + API 速览: - button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用 @@ -1161,6 +1188,7 @@ API 速览: - carousel(props):轮播,items 横排位移,指示点/箭头/←→ 键切换,autoplay 毫秒自动前进且悬停暂停,index 信号可外部受控 - tree(props):树形,递归节点 + 箭头展开/收起 + 点选节点,键盘 Enter 选中、→ 展开、← 收起,禁用节点不响应 - upload(props):文件选择,点击或拖拽入列,清单可逐个移除,onChange 回传文件数组(不内置上传请求) +- notification(props):右下角通知卡片(标题 + 描述 + 操作区),返回 { el, close } 句柄,缺省常驻需手动关闭 一行用法: diff --git a/llms.txt b/llms.txt index 4787183..da5e406 100644 --- a/llms.txt +++ b/llms.txt @@ -91,6 +91,7 @@ - carousel(props):轮播,items 横排位移,指示点/箭头/←→ 键切换,autoplay 毫秒自动前进且悬停暂停,index 信号可外部受控 - tree(props):树形,递归节点 + 箭头展开/收起 + 点选节点,键盘 Enter 选中、→ 展开、← 收起,禁用节点不响应 - upload(props):文件选择,点击或拖拽入列,清单可逐个移除,onChange 回传文件数组(不内置上传请求) + - notification(props):右下角通知卡片(标题 + 描述 + 操作区),返回 { el, close } 句柄,缺省常驻需手动关闭 - 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')` - 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号 - 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调 diff --git a/packages/skin/ui/src/index.ts b/packages/skin/ui/src/index.ts index dca01ab..8e64d4e 100644 --- a/packages/skin/ui/src/index.ts +++ b/packages/skin/ui/src/index.ts @@ -18,6 +18,7 @@ export { avatar } from './avatar.js'; export { image } from './image.js'; export { alert } from './alert.js'; export { toast, showToast } from './toast.js'; +export { notification } from './notification.js'; export { tooltip } from './tooltip.js'; export { popover } from './popover.js'; export { flip } from './position.js'; diff --git a/packages/skin/ui/src/notification.ts b/packages/skin/ui/src/notification.ts new file mode 100644 index 0000000..81378c7 --- /dev/null +++ b/packages/skin/ui/src/notification.ts @@ -0,0 +1,56 @@ +import { h } from '@ui-arch/frame'; +import type { NotificationProps } from './types.js'; + +let host: HTMLElement | null = null; // 右下角堆叠容器:首条通知出现时懒创建 +const MAX_STACK = 3; // 同屏最多条数,超出挤掉最早一条 + +/*** 确保堆叠容器存在:fixed 挂 body 末尾,与右上角 toast 分区不打架 ***/ +function ensureHost(): HTMLElement { + if (host) return host; + host = h('div', { class: 'ua-notice-host' }); + document.body.appendChild(host); + return host; +} + +/*** 退场并移除:摘 ua-open 触发过渡,transitionend 与 320ms 超时二选一兜底 ***/ +function dismiss(item: HTMLElement): void { + item.classList.remove('ua-open'); + item.addEventListener('transitionend', () => item.remove(), { once: true }); + window.setTimeout(() => item.isConnected && item.remove(), 320); // reduced-motion 下不触发 transition +} + +/*** 通知卡片:标题 + 描述 + 可选操作区,返回 { el, close } 句柄;duration 为 0 表示常驻 ***/ +export function notification(props: NotificationProps): { el: HTMLElement; close: () => void } { + // —— 状态集中声明 —— + const duration = props.duration ?? 0; // 缺省常驻:通知通常需要用户读完 + const closable = props.closable ?? true; // 缺省带关闭按钮 + let timer = 0; // 自动关闭计时器句柄 + const closeBtn = h('button', { type: 'button', class: 'ua-notice-close', 'aria-label': '关闭通知' }, '×') as HTMLButtonElement; // 关闭按钮,回调在下方绑定 + const el = h( + 'div', + { class: `ua-notice ua-notice-${props.variant ?? 'info'}`, role: 'alert' }, + h( + 'div', + { class: 'ua-notice-head' }, + h('span', { class: 'ua-notice-dot', 'aria-hidden': 'true' }), + h('span', { class: 'ua-notice-title' }, props.title), + closable ? closeBtn : null, + ), + props.description ? h('div', { class: 'ua-notice-desc' }, props.description) : null, + props.action ? h('div', { class: 'ua-notice-action' }, props.action) : null, + ) as HTMLElement; + + /*** 关闭:清计时器后走退场动画 ***/ + const close = () => { + window.clearTimeout(timer); + dismiss(el); + }; + closeBtn.addEventListener('click', () => close()); + + const stack = ensureHost(); + if (stack.childElementCount >= MAX_STACK) stack.firstElementChild?.remove(); // 超出上限挤掉最早一条 + stack.appendChild(el); + requestAnimationFrame(() => el.classList.add('ua-open')); // 下一帧入场,保证过渡生效 + if (duration > 0) timer = window.setTimeout(() => dismiss(el), duration); // 常驻(0)不设定时器 + return { el, close }; +} diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 85a2b15..346562a 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -420,6 +420,16 @@ export interface ListProps { onItemClick?: (item: ListItem, index: number) => void; // 条目点击回调;提供后条目可点击并带 hover 反馈 } +/*** 通知入参 ***/ +export interface NotificationProps { + title: string; // 通知标题(必填) + description?: string; // 补充说明;缺省不渲染 + variant?: 'info' | 'success' | 'warning' | 'danger'; // 语义色;缺省 info + duration?: number; // 自动关闭毫秒;缺省 0 = 常驻(通知通常要读完) + closable?: boolean; // 是否显示关闭按钮;缺省 true + action?: unknown; // 操作区内容(如按钮);缺省不渲染 +} + /*** 文件选择入参 ***/ export interface UploadProps { accept?: string; // 接受的类型(同原生 accept),如 'image/*,.pdf' diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 0977e12..2a14ba7 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -56,7 +56,8 @@ "list(props):列表,header/footer 外壳 + 条目 avatar/title/description/extra,onItemClick 让条目可点击,items 为空回落空状态", "carousel(props):轮播,items 横排位移,指示点/箭头/←→ 键切换,autoplay 毫秒自动前进且悬停暂停,index 信号可外部受控", "tree(props):树形,递归节点 + 箭头展开/收起 + 点选节点,键盘 Enter 选中、→ 展开、← 收起,禁用节点不响应", - "upload(props):文件选择,点击或拖拽入列,清单可逐个移除,onChange 回传文件数组(不内置上传请求)" + "upload(props):文件选择,点击或拖拽入列,清单可逐个移除,onChange 回传文件数组(不内置上传请求)", + "notification(props):右下角通知卡片(标题 + 描述 + 操作区),返回 { el, close } 句柄,缺省常驻需手动关闭" ], "keyTypes": [ "ButtonProps", @@ -1817,6 +1818,52 @@ "键盘:区域可 Tab 聚焦,Enter/空格 打开选择器;拖拽时区域高亮,拖出恢复", "disabled 时不可点选、不接受拖拽;已选清单的 × 会触发 onChange 回传剩余文件" ] + }, + { + "id": "notification", + "title": "通知卡片 Notification", + "demo": "sec-notification", + "props": [ + { + "n": "title", + "t": "string", + "r": 1, + "d": "通知标题" + }, + { + "n": "description", + "t": "string", + "d": "补充说明;缺省不渲染" + }, + { + "n": "variant", + "t": "'info' | 'success' | 'warning' | 'danger'", + "d": "语义色(左侧条与圆点);缺省 info" + }, + { + "n": "duration", + "t": "number", + "d": "自动关闭毫秒;缺省 0 = 常驻不自动关" + }, + { + "n": "closable", + "t": "boolean", + "d": "是否显示关闭按钮;缺省 true" + }, + { + "n": "action", + "t": "unknown", + "d": "操作区内容(如按钮);缺省不渲染" + } + ], + "example": "const { close } = notification({ title: '导出完成', description: 'report.csv 已开始下载', variant: 'success', duration: 4000 }); // close() 手动关闭", + "steps": [ + "调用即入列:右下角堆叠,同屏最多 3 条,超出自动挤掉最早一条", + "缺省常驻(duration 0),需要自动消失就传毫秒数,如 duration: 4000", + "返回 { el, close } 句柄:程序化关闭用 close(),需要自己挂交互就用 el", + "action 放操作区(常见为按钮);variant 决定左侧条与圆点语义色", + "与 toast 的分工:toast 右上角轻量瞬时,notification 右下角可长读、可操作" + ] } ] } diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index d8c1341..9fbb6c1 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -2282,3 +2282,84 @@ button.ua-step-dot:hover { transition: none; } } + +/* —— 通知卡片 —— */ +.ua-notice-host { + position: fixed; + right: var(--ua-space-4); + bottom: var(--ua-space-4); + display: flex; + flex-direction: column; + gap: var(--ua-space-2); + z-index: 60; + pointer-events: none; /* 容器不挡点击,卡片自身恢复 */ +} +.ua-notice { + width: 300px; + padding: var(--ua-space-3); + border: 1px solid var(--ua-color-border); + border-left: 3px solid var(--ua-color-accent); + border-radius: var(--ua-radius-md); + background: var(--ua-color-bg); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + pointer-events: auto; + opacity: 0; + transform: translateY(8px); + transition: opacity 0.2s ease, transform 0.2s ease; +} +.ua-notice-open { + opacity: 1; + transform: none; +} +.ua-notice-success { border-left-color: var(--ua-color-success); } +.ua-notice-warning { border-left-color: var(--ua-color-warning); } +.ua-notice-danger { border-left-color: var(--ua-color-danger); } +.ua-notice-head { + display: flex; + align-items: center; + gap: var(--ua-space-1); +} +.ua-notice-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--ua-color-accent); + flex: none; +} +.ua-notice-success .ua-notice-dot { background: var(--ua-color-success); } +.ua-notice-warning .ua-notice-dot { background: var(--ua-color-warning); } +.ua-notice-danger .ua-notice-dot { background: var(--ua-color-danger); } +.ua-notice-title { + flex: 1; + font-size: var(--ua-font-size-md); + font-weight: 600; + color: var(--ua-color-text); +} +.ua-notice-close { + border: none; + background: none; + color: var(--ua-color-text-muted); + cursor: pointer; + line-height: 1; + padding: 0 2px; +} +.ua-notice-close:hover { + color: var(--ua-color-text); +} +.ua-notice-desc { + margin-top: var(--ua-space-1); + font-size: var(--ua-font-size-sm); + color: var(--ua-color-text-muted); + line-height: 1.5; +} +.ua-notice-action { + margin-top: var(--ua-space-2); + display: flex; + gap: var(--ua-space-1); + justify-content: flex-end; +} +@media (prefers-reduced-motion: reduce) { + .ua-notice { + transition: none; + } +}