From 0e385cdb3a1e39c505723eb2722ab520f11b0196 Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 10:20:45 +0800 Subject: [PATCH] =?UTF-8?q?iter(g08):=20=E6=96=B0=E5=A2=9E=20segmented=20?= =?UTF-8?q?=E5=88=86=E6=AE=B5=E6=8E=A7=E5=88=B6=E5=99=A8=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/index.json | 53 ++++++++++++++++++++++++-- AGENTS.md | 2 +- ARCHITECTURE.md | 2 +- README.md | 8 ++-- apps/demo/main.ts | 30 +++++++++++++++ llms-full.txt | 30 ++++++++++++++- llms.txt | 1 + packages/skin/ui/src/index.ts | 1 + packages/skin/ui/src/segmented.ts | 63 +++++++++++++++++++++++++++++++ packages/skin/ui/src/types.ts | 13 ++++++- packages/skin/ui/ua.manifest.json | 51 ++++++++++++++++++++++++- packages/skin/ui/ui.css | 62 ++++++++++++++++++++++++++++++ 12 files changed, 302 insertions(+), 14 deletions(-) create mode 100644 packages/skin/ui/src/segmented.ts diff --git a/.ua/index.json b/.ua/index.json index fefc562..c920b4c 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T02:16:50.161Z", + "generatedAt": "2026-10-26T02:20:07.266Z", "repoRoot": ".", "packages": [ { @@ -177,7 +177,8 @@ "formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄", "dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core", "image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽", - "timeline(props):时间线,items 按序渲染纵向事件流(标题 / 时间 / 正文 + 语义色节点),mode 控制轴线左右,pending 追加进行中节点" + "timeline(props):时间线,items 按序渲染纵向事件流(标题 / 时间 / 正文 + 语义色节点),mode 控制轴线左右,pending 追加进行中节点", + "segmented(props):分段控制器,胶囊分段 + 滑动滑块,value 信号双向绑定,←→ 环形导航,整组或单项可禁用" ], "keyTypes": [ "ButtonProps", @@ -216,7 +217,8 @@ "DrawerProps", "AvatarProps", "TimelineItem", - "TimelineProps" + "TimelineProps", + "SegmentedProps" ] }, "usage": "const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')", @@ -1616,6 +1618,51 @@ "需要轴线居右(如右侧栏时间流)时传 mode: 'right',条目与节点整体镜像", "content 为纯文本;需要按钮等交互内容时改用 accordion / card 组合" ] + }, + { + "id": "segmented", + "title": "分段控制器 Segmented", + "demo": "sec-segmented", + "props": [ + { + "n": "options", + "t": "OptionItem[]", + "r": 1, + "d": "分段选项列表(互斥),每项含 value / label / disabled" + }, + { + "n": "value", + "t": "Signal", + "r": 1, + "d": "当前选中值信号,双向绑定(点击写回,外部赋值即同步滑块)" + }, + { + "n": "size", + "t": "'sm' | 'md'", + "d": "尺寸档位;缺省 md", + "df": "md" + }, + { + "n": "disabled", + "t": "boolean | Signal", + "d": "整组禁用:布尔直接置位,传信号则动态同步;单项禁用走 options[].disabled" + }, + { + "n": "onChange", + "t": "(value: string) => void", + "k": 1, + "d": "切换回调(禁用项与重复点击当前项都不触发)" + } + ], + "example": "segmented({ options: [{ value: 'day', label: '日' }, { value: 'week', label: '周' }], value: range })", + "steps": [ + "和 tabs 的分工:tabs 是页签(下划线指示条 + 面板语义),segmented 是紧凑互斥切换(视图 / 单位 / 时间粒度)", + "value 必须传信号:点击写回信号,外部改信号滑块自动平移,不要在回调里另存一份状态", + "options[].disabled 置灰单项;disabled 传布尔或信号则整组失效(连滑块一起置灰)", + "键盘:←→ 在可用项间环形移动并选中,Tab 只落在当前项上(漫游 tabindex)", + "滑块几何由组件测量按钮位置得到,不要在外部改按钮宽度 / 内边距,否则需自行重渲染", + "需要带图标或更复杂布局的分段,改用 radioGroup + 自定义样式" + ] } ] }, diff --git a/AGENTS.md b/AGENTS.md index 720697c..a740fa3 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):35 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌 +- `packages/skin/ui`(@ui-arch/ui):36 个 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 c0156ea..585ebc2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -34,7 +34,7 @@ ui-arch-mvp/ │ │ ├── table-core/ # 表格状态内核 │ │ └── frame/ # 信号 + hyperscript 微框架 │ ├── skin/ # 皮肤层(DOM 组件,可发布) -│ │ └── ui/ # 35 个组件 + ui.css +│ │ └── ui/ # 36 个组件 + ui.css │ ├── design/ # 资产层(可发布) │ │ └── tokens/ # --ua-* 设计令牌 │ └── tools/ # 工程层(private,不发布) diff --git a/README.md b/README.md index 0635194..615c603 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ npm run demo # 打开演示:apps/demo/dist/demo.html npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全量文档 llms-full.txt ``` -## 组件(共 35 个) +## 组件(共 36 个) -accordion · alert · avatar · badge · breadcrumb · button · card · checkbox · dataTable · divider · drawer · empty · form · image · inputNumber · menu · modal · pagination · popover · progress · radioGroup · rate · select · skeleton · slider · spinner · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip +accordion · alert · avatar · badge · breadcrumb · button · card · checkbox · dataTable · divider · drawer · empty · form · image · inputNumber · menu · modal · pagination · popover · progress · radioGroup · rate · segmented · select · skeleton · slider · spinner · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip -分组:表单(button / textField / inputNumber / textarea / checkbox / switchButton / select / radioGroup / slider / rate / form / formItem)、反馈(alert / toast / spinner / progress / skeleton / badge / empty)、浮层(tooltip / popover / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper)、数据(dataTable / tag / avatar / card)、其他(divider / timeline)。 +分组:表单(button / textField / inputNumber / textarea / checkbox / switchButton / select / radioGroup / slider / rate / segmented / form / formItem)、反馈(alert / toast / spinner / progress / skeleton / badge / empty)、浮层(tooltip / popover / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper)、数据(dataTable / tag / avatar / card)、其他(divider / timeline)。 ## 目录 @@ -25,7 +25,7 @@ accordion · alert · avatar · badge · breadcrumb · button · card · checkbo |---|---| | `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:35 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) | +| `packages/skin/ui` | @ui-arch/ui:36 个 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 8d40c21..0b91136 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -31,6 +31,7 @@ import { progress, radioGroup, rate, + segmented, select, showToast, skeleton, @@ -152,6 +153,9 @@ const menuPick = signal(''); // —— 禁用态演示:开关信号同时绑定 switch 的 checked 与 checkbox 的 disabled —— const locked = signal(true); +// —— 分段控制器:两个实例共用同一信号,演示信号驱动同步与整组禁用 —— +const range = signal('week'); + // —— 时间线:false 时末尾显示「进行中」虚线节点,true 表示流程已结束 —— const flowDone = signal(false); @@ -444,6 +448,32 @@ const content = h( button({ label: '导出', variant: 'ghost' }), ), ), + section( + 'sec-segmented', + '分段控制器', + 'segmented:胶囊分段 + 滑动滑块,value 信号双向绑定;两个实例共用信号即自动同步,整组禁用走信号。', + h( + 'div', + { class: 'row' }, + segmented({ + options: [ + { value: 'day', label: '日' }, + { value: 'week', label: '周' }, + { value: 'month', label: '月' }, + { value: 'year', label: '年', disabled: true }, + ], + value: range, + }), + h('span', { class: 'out' }, () => `当前粒度:${range()}`), + ), + h( + 'div', + { class: 'row' }, + segmented({ options: [{ value: 'day', label: '日' }, { value: 'week', label: '周' }, { value: 'month', label: '月' }], value: range, size: 'sm' }), + segmented({ options: [{ value: 'day', label: '日' }, { value: 'week', label: '周' }], value: range, size: 'sm', disabled: locked }), + h('span', { class: 'out' }, () => (locked() ? '第二个分段已禁用(同上「锁定」开关)' : '第二个分段可用')), + ), + ), section( 'sec-timeline', '时间线', diff --git a/llms-full.txt b/llms-full.txt index ed81afa..99bcdf1 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T02:16:50.161Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T02:20:07.266Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -923,6 +923,33 @@ timeline({ items: [{ title: '提交审批', time: '09:12', variant: 'success' }, 现场演示:apps/docs/demo.html#sec-timeline +### segmented — 分段控制器 Segmented + +参数: + +- options(必填):OptionItem[] —— 分段选项列表(互斥),每项含 value / label / disabled +- value(必填):Signal —— 当前选中值信号,双向绑定(点击写回,外部赋值即同步滑块) +- size(默认 md):'sm' | 'md' —— 尺寸档位;缺省 md +- disabled:boolean | Signal —— 整组禁用:布尔直接置位,传信号则动态同步;单项禁用走 options[].disabled +- onChange(回调):(value: string) => void —— 切换回调(禁用项与重复点击当前项都不触发) + +示例: + +``` +segmented({ options: [{ value: 'day', label: '日' }, { value: 'week', label: '周' }], value: range }) +``` + +手把手: + +1. 和 tabs 的分工:tabs 是页签(下划线指示条 + 面板语义),segmented 是紧凑互斥切换(视图 / 单位 / 时间粒度) +2. value 必须传信号:点击写回信号,外部改信号滑块自动平移,不要在回调里另存一份状态 +3. options[].disabled 置灰单项;disabled 传布尔或信号则整组失效(连滑块一起置灰) +4. 键盘:←→ 在可用项间环形移动并选中,Tab 只落在当前项上(漫游 tabindex) +5. 滑块几何由组件测量按钮位置得到,不要在外部改按钮宽度 / 内边距,否则需自行重渲染 +6. 需要带图标或更复杂布局的分段,改用 radioGroup + 自定义样式 + +现场演示:apps/docs/demo.html#sec-segmented + API 速览: - button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用 @@ -961,6 +988,7 @@ API 速览: - dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core - image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽 - timeline(props):时间线,items 按序渲染纵向事件流(标题 / 时间 / 正文 + 语义色节点),mode 控制轴线左右,pending 追加进行中节点 +- segmented(props):分段控制器,胶囊分段 + 滑动滑块,value 信号双向绑定,←→ 环形导航,整组或单项可禁用 一行用法: diff --git a/llms.txt b/llms.txt index 75c1e0b..ce7fd46 100644 --- a/llms.txt +++ b/llms.txt @@ -83,6 +83,7 @@ - dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core - image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽 - timeline(props):时间线,items 按序渲染纵向事件流(标题 / 时间 / 正文 + 语义色节点),mode 控制轴线左右,pending 追加进行中节点 + - segmented(props):分段控制器,胶囊分段 + 滑动滑块,value 信号双向绑定,←→ 环形导航,整组或单项可禁用 - 用法: `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 46b2d07..53b2f81 100644 --- a/packages/skin/ui/src/index.ts +++ b/packages/skin/ui/src/index.ts @@ -25,6 +25,7 @@ export { pagination } from './pagination.js'; export { spinner } from './spinner.js'; export { progress } from './progress.js'; export { tabs } from './tabs.js'; +export { segmented } from './segmented.js'; export { stepper } from './stepper.js'; export { rate } from './rate.js'; export { empty } from './empty.js'; diff --git a/packages/skin/ui/src/segmented.ts b/packages/skin/ui/src/segmented.ts new file mode 100644 index 0000000..bcf85b6 --- /dev/null +++ b/packages/skin/ui/src/segmented.ts @@ -0,0 +1,63 @@ +import { effect, h } from '@ui-arch/frame'; +import type { SegmentedProps } from './types.js'; + +/*** 分段控制器:胶囊分段条 + 滑动滑块,value 信号双向绑定,←→/Home/End 环形导航,整组或单项可禁用 ***/ +export function segmented(props: SegmentedProps): HTMLElement { + // —— 状态集中声明 —— + const buttons: HTMLButtonElement[] = []; // 分段按钮,顺序与 options 一致 + const thumb = h('span', { class: 'ua-seg-thumb', 'aria-hidden': 'true' }); // 滑块:仅装饰,不参与可访问性树 + const root = h('div', { class: `ua-segmented ua-segmented-${props.size ?? 'md'}`, role: 'radiogroup' }, thumb); + + /*** 整组禁用判定:布尔直接取,信号在此读取以纳入 effect 依赖 ***/ + const groupOff = (): boolean => (typeof props.disabled === 'function' ? props.disabled() : props.disabled === true); + + /*** 同步选中态、禁用态与滑块几何(挂载前量不到宽度,由 rAF 二次测量兜底) ***/ + const sync = (): void => { + const off = groupOff(); + const box = root.getBoundingClientRect(); + root.classList.toggle('is-disabled', off); + for (const btn of buttons) { + const on = btn.dataset.value === props.value(); + const dead = off || btn.dataset.dead === '1'; // 整组禁用或单项禁用 + btn.classList.toggle('is-active', on); + btn.setAttribute('aria-checked', String(on)); + btn.setAttribute('aria-disabled', String(dead)); + btn.disabled = dead; + btn.tabIndex = on ? 0 : -1; // 漫游 tabindex:仅选中项可 Tab 聚焦 + if (!on) continue; // 滑块只跟选中项走 + const rect = btn.getBoundingClientRect(); + thumb.style.width = `${rect.width}px`; + thumb.style.transform = `translateX(${rect.left - box.left - root.clientLeft}px)`; // 减去容器边框,滑块与按钮严丝合缝 + } + }; + + for (const opt of props.options) { + const btn = h('button', { type: 'button', class: 'ua-seg-btn', role: 'radio' }, opt.label) as HTMLButtonElement; + btn.dataset.value = opt.value; + if (opt.disabled) btn.dataset.dead = '1'; // 单项禁用标记,交给 sync 统一置 disabled + btn.addEventListener('click', () => { + if (btn.disabled) return; // 卫语句:禁用项不响应 + props.value(opt.value); // 用户切换写回信号 + props.onChange?.(opt.value); + }); + buttons.push(btn); + root.append(btn); + } + + root.addEventListener('keydown', (e: KeyboardEvent) => { + const map: Record = { ArrowRight: 1, ArrowLeft: -1 }; // 左右移动,首尾环形 + const steer = map[e.key]; + if (!steer) return; // 卫语句:非方向键放行 + const list = buttons.filter((b) => !b.disabled); + const i = list.indexOf(document.activeElement as HTMLButtonElement); + if (i < 0) return; // 卫语句:焦点不在分段项上 + e.preventDefault(); + const next = list[(i + steer + list.length) % list.length]; // 环形取模 + next.focus(); + next.click(); + }); + + effect(sync); + requestAnimationFrame(sync); // 挂载后再量一次布局,否则滑块宽度为 0 + return root; +} diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 2b36b23..e8341b7 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -59,11 +59,20 @@ export interface DataTableProps { selected?: Signal; // 受控选中行信号:与内核选择双向同步,供批量操作读取 } -/*** 选项条目:select / radioGroup / tabs 共用的选项形状 ***/ +/*** 选项条目:select / radioGroup / tabs / segmented 共用的选项形状 ***/ export interface OptionItem { value: string; // 选项值(提交/匹配用) label: string; // 选项文案(展示用) - disabled?: boolean; // 仅 tabs 消费:禁用该页签 + disabled?: boolean; // tabs / segmented 消费:禁用该选项 +} + +/*** 分段控制器入参 ***/ +export interface SegmentedProps { + options: OptionItem[]; // 分段选项(互斥) + value: Signal; // 当前选中值信号,双向绑定(点击写回、外部赋值同步) + size?: 'sm' | 'md'; // 尺寸档位;缺省 md + disabled?: boolean | Signal; // 整组禁用:布尔直接置位,传信号则动态同步 + onChange?: (value: string) => void; // 切换回调(禁用项不触发) } /*** 复选框入参 ***/ diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index a66a7b0..5f75420 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -48,7 +48,8 @@ "formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄", "dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core", "image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽", - "timeline(props):时间线,items 按序渲染纵向事件流(标题 / 时间 / 正文 + 语义色节点),mode 控制轴线左右,pending 追加进行中节点" + "timeline(props):时间线,items 按序渲染纵向事件流(标题 / 时间 / 正文 + 语义色节点),mode 控制轴线左右,pending 追加进行中节点", + "segmented(props):分段控制器,胶囊分段 + 滑动滑块,value 信号双向绑定,←→ 环形导航,整组或单项可禁用" ], "keyTypes": [ "ButtonProps", @@ -87,7 +88,8 @@ "DrawerProps", "AvatarProps", "TimelineItem", - "TimelineProps" + "TimelineProps", + "SegmentedProps" ] }, "usage": "const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')", @@ -1487,6 +1489,51 @@ "需要轴线居右(如右侧栏时间流)时传 mode: 'right',条目与节点整体镜像", "content 为纯文本;需要按钮等交互内容时改用 accordion / card 组合" ] + }, + { + "id": "segmented", + "title": "分段控制器 Segmented", + "demo": "sec-segmented", + "props": [ + { + "n": "options", + "t": "OptionItem[]", + "r": 1, + "d": "分段选项列表(互斥),每项含 value / label / disabled" + }, + { + "n": "value", + "t": "Signal", + "r": 1, + "d": "当前选中值信号,双向绑定(点击写回,外部赋值即同步滑块)" + }, + { + "n": "size", + "t": "'sm' | 'md'", + "d": "尺寸档位;缺省 md", + "df": "md" + }, + { + "n": "disabled", + "t": "boolean | Signal", + "d": "整组禁用:布尔直接置位,传信号则动态同步;单项禁用走 options[].disabled" + }, + { + "n": "onChange", + "t": "(value: string) => void", + "k": 1, + "d": "切换回调(禁用项与重复点击当前项都不触发)" + } + ], + "example": "segmented({ options: [{ value: 'day', label: '日' }, { value: 'week', label: '周' }], value: range })", + "steps": [ + "和 tabs 的分工:tabs 是页签(下划线指示条 + 面板语义),segmented 是紧凑互斥切换(视图 / 单位 / 时间粒度)", + "value 必须传信号:点击写回信号,外部改信号滑块自动平移,不要在回调里另存一份状态", + "options[].disabled 置灰单项;disabled 传布尔或信号则整组失效(连滑块一起置灰)", + "键盘:←→ 在可用项间环形移动并选中,Tab 只落在当前项上(漫游 tabindex)", + "滑块几何由组件测量按钮位置得到,不要在外部改按钮宽度 / 内边距,否则需自行重渲染", + "需要带图标或更复杂布局的分段,改用 radioGroup + 自定义样式" + ] } ] } diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 002680a..882aec2 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -1754,3 +1754,65 @@ button.ua-step-dot:hover { transition: none !important; } } + +/* —— 分段控制器:胶囊容器 + 滑动滑块,选中态低视觉比重 —— */ +.ua-segmented { + position: relative; + display: inline-flex; + border: 1px solid var(--ua-color-border); + border-radius: 999px; + background: color-mix(in srgb, var(--ua-color-text) 4%, transparent); + overflow: hidden; +} +.ua-seg-thumb { + position: absolute; + top: 0; + left: 0; + height: 100%; + border-radius: 999px; + background: var(--ua-color-bg); + box-shadow: 0 1px 4px color-mix(in srgb, var(--ua-color-text) 12%, transparent); + transition: transform 0.18s ease, width 0.18s ease; + pointer-events: none; +} +.ua-seg-btn { + position: relative; + z-index: 1; + padding: 5px 14px; + border: 0; + border-radius: 999px; + background: transparent; + color: var(--ua-color-text-muted); + font-size: var(--ua-font-size-md); + cursor: pointer; + transition: color 0.15s; + white-space: nowrap; +} +.ua-seg-btn:hover:not(:disabled) { + color: var(--ua-color-text); +} +.ua-seg-btn:focus-visible { + outline: 2px solid var(--ua-color-accent); + outline-offset: -2px; +} +.ua-seg-btn.is-active { + color: var(--ua-color-accent); + font-weight: 600; +} +.ua-seg-btn:disabled { + color: color-mix(in srgb, var(--ua-color-text-muted) 45%, transparent); + cursor: not-allowed; +} +.ua-segmented-sm .ua-seg-btn { + padding: 3px 10px; + font-size: var(--ua-font-size-sm); +} +.ua-segmented.is-disabled { + opacity: 0.6; +} +@media (prefers-reduced-motion: reduce) { + .ua-seg-thumb, + .ua-seg-btn { + transition: none !important; + } +}