diff --git a/.ua/index.json b/.ua/index.json index 514b9f9..1466f13 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T04:34:05.417Z", + "generatedAt": "2026-10-26T04:35:37.103Z", "repoRoot": ".", "packages": [ { @@ -930,6 +930,22 @@ "t": "() => void", "k": 1, "d": "关闭回调:调用方从数据源移除该项后重新渲染" + }, + { + "n": "checkable", + "t": "boolean", + "df": "false", + "d": "可选中:渲染为筛选片(button + aria-pressed);与 closable 互斥,同时传以 checkable 为准" + }, + { + "n": "checked", + "t": "Signal", + "d": "选中态信号,双向绑定(点击写回、外部赋值同步);不传则组件内部自持" + }, + { + "n": "onChange", + "t": "(checked: boolean) => void", + "d": "选中态变化回调,参数是点击后的新值" } ], "example": "tag({ label: '前端', variant: 'info', closable: true, onClose: () => remove('前端') })", @@ -938,7 +954,8 @@ "按语义选 variant:info 中性标记、success 通过、warning 待处理、danger 已失效", "closable: true 时必须配 onClose:把数据源里的该项删掉再重渲染(组件无状态,不自行摘除节点)", "组合用法:容器里用 renderList 按信号数组渲染 tag 列表,删除即写回信号", - "标签是内联元素,可直接排进 .row 等容器,自动换行由容器决定" + "标签是内联元素,可直接排进 .row 等容器,自动换行由容器决定", + "需要可点的筛选片时传 checkable + checked:点击写回信号,视觉与 aria-pressed 同步,不做整块重渲染(焦点不丢)" ] }, { diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 27d4402..6bbe442 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -165,6 +165,7 @@ const drawerOpen = signal(false); // —— 标签列表:关闭即写回信号,容器按信号重渲染 —— const tags = signal(['设计', '前端', '组件库']); +const chipOn = { 全部: signal(true), 进行中: signal(false), 已完成: signal(false), 已归档: signal(false) }; // 筛选片选中态:每片独立信号,供 tag 双向绑定 // —— 滑块:音量与禁用态演示 —— const volume = signal(40); @@ -306,6 +307,12 @@ const content = h( renderList(box, () => tags().map((t) => tag({ label: t, variant: 'info', closable: true, onClose: () => tags(tags().filter((x) => x !== t)) }))); return box; })(), + (() => { + const chips = h('div', { class: 'row' }); + renderList(chips, () => Object.entries(chipOn).map(([name, on]) => tag({ label: name, checkable: true, checked: on }))); + const out = h('span', { class: 'out' }, () => `已选:${Object.entries(chipOn).filter(([, on]) => on()).map(([name]) => name).join(' / ') || '无'}`); + return h('div', {}, chips, out); + })(), ), section( 'sec-avatar', diff --git a/llms-full.txt b/llms-full.txt index 92945cd..ffc80f1 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T04:34:05.417Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T04:35:37.103Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -442,6 +442,9 @@ badge({ label: '成功', variant: 'success' }) - variant(默认 default):'default' | 'success' | 'warning' | 'danger' | 'info' —— 语义色;缺省 default - closable(默认 false):boolean —— 显示 × 关闭按钮;缺省 false(纯展示) - onClose(回调):() => void —— 关闭回调:调用方从数据源移除该项后重新渲染 +- checkable(默认 false):boolean —— 可选中:渲染为筛选片(button + aria-pressed);与 closable 互斥,同时传以 checkable 为准 +- checked:Signal —— 选中态信号,双向绑定(点击写回、外部赋值同步);不传则组件内部自持 +- onChange:(checked: boolean) => void —— 选中态变化回调,参数是点击后的新值 示例: @@ -456,6 +459,7 @@ tag({ label: '前端', variant: 'info', closable: true, onClose: () => remove(' 3. closable: true 时必须配 onClose:把数据源里的该项删掉再重渲染(组件无状态,不自行摘除节点) 4. 组合用法:容器里用 renderList 按信号数组渲染 tag 列表,删除即写回信号 5. 标签是内联元素,可直接排进 .row 等容器,自动换行由容器决定 +6. 需要可点的筛选片时传 checkable + checked:点击写回信号,视觉与 aria-pressed 同步,不做整块重渲染(焦点不丢) 现场演示:apps/docs/demo.html#sec-tag diff --git a/packages/skin/ui/src/tag.ts b/packages/skin/ui/src/tag.ts index 3862136..5595408 100644 --- a/packages/skin/ui/src/tag.ts +++ b/packages/skin/ui/src/tag.ts @@ -1,15 +1,28 @@ -import { h } from '@ui-arch/frame'; +import { effect, h, signal } from '@ui-arch/frame'; import type { TagProps } from './types.js'; -/*** 标签:语义色小胶囊,可选 × 关闭;与 badge 的分工是 tag 可交互(带关闭回调) ***/ +/*** 标签:语义色小胶囊;checkable 时作筛选片(aria-pressed + 信号双向绑定),否则可选 × 关闭 ***/ export function tag(props: TagProps): HTMLElement { const label = h('span', { class: 'ua-tag-label' }, props.label); - if (!props.closable) { - return h('span', { class: `ua-tag ua-tag-${props.variant ?? 'default'}` }, label); // 纯展示形态 + const base = `ua-tag ua-tag-${props.variant ?? 'default'}`; // 语义色共用 + if (props.checkable) { + const on = props.checked ?? signal(false); // 未受控时内部自持,行为与受控一致 + const btn = h('button', { type: 'button', class: `${base} is-checkable`, 'aria-pressed': String(on()) }, label) as HTMLButtonElement; + btn.addEventListener('click', () => { + on(!on()); // 用户点击写回信号 + props.onChange?.(on()); + }); + effect(() => { + const value = on(); + btn.setAttribute('aria-pressed', String(value)); // 读屏与视觉同源 + btn.classList.toggle('is-checked', value); + }); + return btn; } + if (!props.closable) return h('span', { class: base }, label); // 纯展示形态 return h( 'span', - { class: `ua-tag is-closable ua-tag-${props.variant ?? 'default'}` }, + { class: `${base} is-closable` }, label, h('button', { type: 'button', class: 'ua-tag-close', 'aria-label': `移除 ${props.label}`, onClick: () => props.onClose?.() }, '×'), ); diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 8df3a7d..3f545ab 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -254,6 +254,9 @@ export interface TagProps { variant?: 'default' | 'success' | 'warning' | 'danger' | 'info'; // 语义色;缺省 default closable?: boolean; // 显示 × 关闭按钮;缺省 false onClose?: () => void; // 关闭回调(调用方自行从数据源移除该项) + checkable?: boolean; // 可选中(当筛选片用);与 closable 互斥,同时传时以 checkable 为准 + checked?: Signal; // 选中态信号,双向绑定;不传则组件内部自持 + onChange?: (checked: boolean) => void; // 选中态变化回调(点一下即回调新值) } /*** 图片入参 ***/ diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 2edc8e7..2c87ec2 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -801,6 +801,22 @@ "t": "() => void", "k": 1, "d": "关闭回调:调用方从数据源移除该项后重新渲染" + }, + { + "n": "checkable", + "t": "boolean", + "df": "false", + "d": "可选中:渲染为筛选片(button + aria-pressed);与 closable 互斥,同时传以 checkable 为准" + }, + { + "n": "checked", + "t": "Signal", + "d": "选中态信号,双向绑定(点击写回、外部赋值同步);不传则组件内部自持" + }, + { + "n": "onChange", + "t": "(checked: boolean) => void", + "d": "选中态变化回调,参数是点击后的新值" } ], "example": "tag({ label: '前端', variant: 'info', closable: true, onClose: () => remove('前端') })", @@ -809,7 +825,8 @@ "按语义选 variant:info 中性标记、success 通过、warning 待处理、danger 已失效", "closable: true 时必须配 onClose:把数据源里的该项删掉再重渲染(组件无状态,不自行摘除节点)", "组合用法:容器里用 renderList 按信号数组渲染 tag 列表,删除即写回信号", - "标签是内联元素,可直接排进 .row 等容器,自动换行由容器决定" + "标签是内联元素,可直接排进 .row 等容器,自动换行由容器决定", + "需要可点的筛选片时传 checkable + checked:点击写回信号,视觉与 aria-pressed 同步,不做整块重渲染(焦点不丢)" ] }, { diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index da0544e..918cb93 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -708,6 +708,37 @@ transform: scale(1.15); } +/* —— 可选标签(筛选片)—— */ +.ua-tag.is-checkable { + font: inherit; + color: var(--ua-color-text-muted); + background: transparent; + cursor: pointer; + transition: transform 0.15s, border-color 0.15s, background 0.15s, color 0.15s; +} +.ua-tag.is-checkable:hover { + transform: translateY(-1px); + color: var(--ua-color-text); +} +.ua-tag.is-checkable:active { + transform: scale(0.97); +} +.ua-tag.is-checkable:focus-visible { + outline: 2px solid color-mix(in srgb, var(--ua-color-accent) 45%, transparent); + outline-offset: 2px; +} +.ua-tag.is-checked { + border-color: color-mix(in srgb, var(--ua-color-accent) 55%, transparent); + background: color-mix(in srgb, var(--ua-color-accent) 10%, var(--ua-color-bg)); + color: var(--ua-color-text); +} + +@media (prefers-reduced-motion: reduce) { + .ua-tag.is-checkable { transition: none; } + .ua-tag.is-checkable:hover, + .ua-tag.is-checkable:active { transform: none; } +} + /* —— 滑块 —— */ .ua-slider { display: inline-flex;