@icen.ai/ui 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +15 -9
- package/README.md +8 -3
- package/dist/base.css +4 -3
- package/dist/behaviors/accordion.d.mts +5 -2
- package/dist/behaviors/accordion.mjs +36 -11
- package/dist/behaviors/ai-chat.d.mts +3 -1
- package/dist/behaviors/ai-chat.mjs +66 -43
- package/dist/behaviors/ai-composer.d.mts +28 -3
- package/dist/behaviors/ai-composer.mjs +291 -209
- package/dist/behaviors/ai-core.d.mts +9 -4
- package/dist/behaviors/ai-core.mjs +21 -8
- package/dist/behaviors/ai-diff.d.mts +2 -2
- package/dist/behaviors/ai-diff.mjs +172 -41
- package/dist/behaviors/ai-panel.d.mts +11 -8
- package/dist/behaviors/ai-panel.mjs +185 -110
- package/dist/behaviors/ai-provider.mjs +54 -48
- package/dist/behaviors/ai-tool.d.mts +12 -4
- package/dist/behaviors/ai-tool.mjs +311 -263
- package/dist/behaviors/ai-tools.d.mts +9 -6
- package/dist/behaviors/ai-tools.mjs +259 -155
- package/dist/behaviors/back-top.mjs +16 -1
- package/dist/behaviors/carousel.d.mts +5 -2
- package/dist/behaviors/carousel.mjs +53 -26
- package/dist/behaviors/charts.d.mts +68 -22
- package/dist/behaviors/charts.mjs +221 -150
- package/dist/behaviors/command-palette.d.mts +40 -6
- package/dist/behaviors/command-palette.mjs +143 -15
- package/dist/behaviors/context-menu.d.mts +21 -4
- package/dist/behaviors/context-menu.mjs +122 -38
- package/dist/behaviors/controls.d.mts +59 -0
- package/dist/behaviors/controls.mjs +601 -0
- package/dist/behaviors/copy.d.mts +5 -2
- package/dist/behaviors/copy.mjs +37 -12
- package/dist/behaviors/datatable.d.mts +12 -0
- package/dist/behaviors/datatable.mjs +226 -43
- package/dist/behaviors/date-picker.d.mts +2 -2
- package/dist/behaviors/date-picker.mjs +171 -32
- package/dist/behaviors/dropdown.d.mts +6 -2
- package/dist/behaviors/dropdown.mjs +164 -25
- package/dist/behaviors/events.d.mts +330 -0
- package/dist/behaviors/events.mjs +208 -0
- package/dist/behaviors/input.mjs +25 -12
- package/dist/behaviors/modal.d.mts +24 -5
- package/dist/behaviors/modal.mjs +76 -10
- package/dist/behaviors/nav.d.mts +1 -1
- package/dist/behaviors/nav.mjs +33 -7
- package/dist/behaviors/notification.d.mts +20 -4
- package/dist/behaviors/notification.mjs +53 -73
- package/dist/behaviors/popover.d.mts +39 -3
- package/dist/behaviors/popover.mjs +90 -4
- package/dist/behaviors/select.d.mts +4 -2
- package/dist/behaviors/select.mjs +161 -24
- package/dist/behaviors/sidebar.d.mts +5 -2
- package/dist/behaviors/sidebar.mjs +43 -13
- package/dist/behaviors/slider.d.mts +5 -2
- package/dist/behaviors/slider.mjs +54 -43
- package/dist/behaviors/split-pane.d.mts +6 -2
- package/dist/behaviors/split-pane.mjs +62 -21
- package/dist/behaviors/tabs.d.mts +3 -2
- package/dist/behaviors/tabs.mjs +65 -14
- package/dist/behaviors/tag-input.d.mts +5 -2
- package/dist/behaviors/tag-input.mjs +61 -21
- package/dist/behaviors/theme.d.mts +13 -2
- package/dist/behaviors/theme.mjs +48 -4
- package/dist/behaviors/toast.d.mts +30 -8
- package/dist/behaviors/toast.mjs +74 -7
- package/dist/behaviors/tree.d.mts +5 -2
- package/dist/behaviors/tree.mjs +38 -15
- package/dist/behaviors/upload.mjs +21 -23
- package/dist/cli.mjs +1 -1
- package/dist/components/ai-panel.css +1 -1
- package/dist/components/ai-tool-call.mjs +1 -1
- package/dist/components/badge.css +1 -1
- package/dist/components/breadcrumb.css +0 -32
- package/dist/components/chart-calendar.mjs +1 -2
- package/dist/components/chart-pie.css +0 -6
- package/dist/components/charts.css +1 -1
- package/dist/components/content.css +3 -16
- package/dist/components/datatable.css +1 -1
- package/dist/components/date-picker.css +1 -1
- package/dist/components/notification.css +2 -3
- package/dist/components/pagination.css +3 -1
- package/dist/components/pagination.mjs +1 -0
- package/dist/components/rating.mjs +1 -0
- package/dist/components/segmented.css +9 -1
- package/dist/components/segmented.mjs +1 -1
- package/dist/components/select.css +67 -4
- package/dist/components/steps.css +13 -3
- package/dist/components/steps.mjs +1 -0
- package/dist/components/switch.css +3 -1
- package/dist/components/switch.mjs +1 -0
- package/dist/components/table.mjs +1 -0
- package/dist/components/tabs.css +23 -0
- package/dist/components/tag-input.css +1 -6
- package/dist/components/upload.css +0 -15
- package/dist/index.d.mts +12 -10
- package/dist/index.mjs +2700 -1460
- package/dist/registry.json +42 -13
- package/dist/retro-effects.css +3 -16
- package/dist/tokens/retro-effects.css +3 -16
- package/dist/tokens/style-profiles.css +3 -2
- package/dist/tokens/typography.css +2 -0
- package/dist/tokens.css +5 -2
- package/dist/ui.css +138 -98
- package/docs/spec/ai-native.md +7 -4
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -50,7 +50,7 @@ c.destroy();
|
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
- 省略 `type` 时 `inferChartType` 自动选型(ISO 日期/年月/周几/版本号 v0.8/纯年份等时序标签 → line)。
|
|
53
|
-
- 事件:`icen:chart-
|
|
53
|
+
- 事件:`icen:chart-hover`(detail.phase ∈ enter/move/leave)/ `-click / -dblclick / -contextmenu / -legend-toggle / -legend-visibility`;内置 tooltip 门户(`tooltip:false` 关闭只派事件),tooltip 最大宽吃图表根 `data-panel-max`;图表头统一带**图例眼睛**(隐藏/显示全部指标)。直调底层渲染器同样有交互委托(`bindChartEvents`)。
|
|
54
54
|
- 底层渲染器(同 chrome,非 AI 场景直用):`renderVBar/renderHBar/renderStack/renderDonut/renderLine/renderArea/renderRadar/renderHeatmap/renderCalendar/renderSparkline/renderGauge/renderScatter`。
|
|
55
55
|
|
|
56
56
|
## AI 原生族 — 7 态状态机 + kind 注册表
|
|
@@ -174,7 +174,7 @@ bun run build # tsup(src/behaviors/*.ts → dist/behaviors/*.mjs + index.m
|
|
|
174
174
|
## 规模
|
|
175
175
|
|
|
176
176
|
- **56 个组件 CSS**(含 charts 基座 + 10 个细分图表 CSS(+v0.8 chart-scatter)+ 10 个 v0.5 新增:copy/tag/badge/scroll-area/notification/back-top/command-palette/date-picker/toolbar/split-pane + 4 个 v0.7 AI 族合并文件:ai-chat/ai-tool/ai-diff/ai-panel)
|
|
177
|
-
- **
|
|
177
|
+
- **35 个 behaviors TS**(v0.8.x:+events 统一事件体系 +controls 控件族 +ai-tools;charts 通用层见图表节)(theme/tabs/toast/copy/input/select/slider/tag-input/upload/modal/dropdown/popover/context-menu/accordion/tree/carousel/charts/nav/sidebar/datatable/notification/back-top/command-palette/date-picker/split-pane/events/controls + 8 个 AI 族:ai-core/ai-chat/ai-composer/ai-tool/ai-provider/ai-diff/ai-panel/ai-tools)
|
|
178
178
|
- **77 个组件 slug**(kit 一行入口 / CLI / 文档站侧栏共用 SLUGS 清单)
|
|
179
179
|
- **4 个 token 文件**:colors.css(6 预设 × 明暗)、style-profiles.css(modern/retro/terminal)、typography.css、retro-effects.css(可选)
|
|
180
180
|
|
|
@@ -197,22 +197,28 @@ bun run build # tsup(src/behaviors/*.ts → dist/behaviors/*.mjs + index.m
|
|
|
197
197
|
- **交互基元**:`base.css` 提供 6 个组件地基 `.pressable` / `.control` / `.field` / `.lift` / `.surface-elevated` / `.focus-ring`。`style-profiles.css` 的 `.style-retro` 段选择器引用这 6 个类名——改名必须同步修 style-profiles.css,否则 retro 风格失效。共享 keyframes(`icen-pop-in` / `icen-spin` / `icen-skeleton-pulse`)也在 base.css,组件动画复用、不各自重定义。
|
|
198
198
|
- **z-index 标尺**:浮层一律 portal 到 body,z-index 取 `--z-base/raised/sticky/chrome/toast/banner/dialog/popover/tooltip/fx`(0.6.0 新增 `--z-fx`,retro-effects 特效层用)。
|
|
199
199
|
- **浮层 portal 家族**:popover / dropdown / context-menu / command-palette / date-picker / **select**(0.6.0 起 panel 也 portal 到 body,与 popover 共用定位模式)。
|
|
200
|
+
- **凹槽 × 尺寸 × 事件三契约(全库统一心智模型,v0.8 收敛)**:
|
|
201
|
+
- **凹槽(内容)**:标记驱动组件 = 任意 HTML 凹槽(契约在各组件 CSS 头注释,如 modal 的 `.modal-body`、select 的 `.select-option` 内可写色卡/图标/多行);函数渲染组件 = 类型化 model(一律 `textContent` 防 XSS)+ 指定挂载点(`renderAiMessage` 的 `handle.body`、datatable 的 `columns.render → Node`)。toast / notification 保持纯文本是安全纪律,不开 HTML 口子。
|
|
202
|
+
- **尺寸(PanelSizing)**:所有浮层面板共享 `behaviors/popover.ts` 的 `PanelSizing` 类型(`width 固定(最高优先)/ minWidth / maxWidth / maxHeight`)与同名 data 属性 `data-panel-width / data-panel-min / data-panel-max / data-panel-max-height`(挂组件根;context-menu 等无宿主面板的走 `opts.sizing` 程序面,datatable 右键自动从宿主读)。共享解析 `readPanelSizing(el)` / `resolvePanelSizing(el, overrides)`(程序面 > 属性面)+ `sizingToLayout`(锚定布局)/ `applyPanelSizing(panel, sizing | null)`(自建面板内联;固定宽连带盖掉 CSS 侧 max-width;**带视口夹取**——宽一律夹到视口 −24px;**传 null 复位全部内联尺寸**,单例面板跨实例复用防串味,date-picker 即此用法)。布局侧 `side` 支持 `top/bottom/left/right`(左右侧自动翻转)。已接入:select / popover(openPopover) / dropdown / context-menu / date-picker / command-palette / modal / ai-composer 三弹层(`popoverSizing` opts > composer 根 data-panel-* > 内置默认)/ datatable 筛选与列显隐面板。新增浮层组件必须走此契约,禁止再写死宽高。
|
|
203
|
+
- **事件(三层体系,`behaviors/events.ts`)**:①手势层——任意容器挂 `data-gestures="click dblclick contextmenu longpress text-select"`(空格分隔)即开通归一化 `icen:` 手势事件(click 与 dblclick 同开时单击延迟 220ms,双击到达即取消;位移 >5px 视为拖拽不派发);②协议层——全部 CustomEvent,`bubbles + composed + cancelable`;detail 契约全量登记在 `IcenEventMap`(onIcen 推断 / emitIcen 校验双侧同表,新增事件必须登记,拼错或 detail 不符编译期报错);从 document 派发(如 `icen:ai-done`/`icen:ai-usage`)= 全局广播——传播路径只经 [document, window],只有全局形态能收到(`within` 为普通元素时路径不经过它,收不到,要收广播用全局形态);③策略层——`setEventPolicy(type, fn|null)` 只收五种手势事件(编译期收死),给事件注册全站默认行为,一处赋予/覆盖/禁用,`getEventPolicy`/`resetEventPolicies` 为对称读取/清空口(调试与热重载)。绑定层 `onIcen`:类型推断 + 选择器委托 + `{ within, signal, once }`,返回解绑函数;派生口 `emitIcen`(勿裸写 new CustomEvent);`initGestures()` 幂等并返回销毁函数(`destroyGestures()` 同效),组件作者派发领域事件同样必须先在 `IcenEventMap` 登记。拖拽不在手势层(文件走 upload,排序/分割走组件自带)。
|
|
204
|
+
- **控件族(`behaviors/controls.ts`)**:纯 CSS 控件的行为承载统一在此(switch/checkbox/radio 三态、stepper、segmented、steps、rating、pagination、静态表 th[data-sort] 排序),七 init 全部「document 委托 + 幂等 + 返回销毁」同款骨架;事件 `icen:switch-change / check-change / stepper-change / segmented-change / step-change / rating-change / page-change / sort-change`。slugs 映射走 SLUG_BEHAVIOR(segmented 同时带 input 的 initInput,kit 由 SLUG_EXPORTS 三项并存解决;controls 内转发 initInput 仅为 kit 服务,**根入口 index.ts 对 controls 用具名导出**——export * 会与 input 的 initInput 构成 ESM 歧义名被静默剔除)。
|
|
200
205
|
- **事件前缀纪律**:behavior 派发的自定义事件统一 `icen:` 前缀(如 tabs 的 `icen:tab-change`、upload 的 `icen:upload`)。otp 为兼容双发 `icen:otp-complete`(新)与 `otp:complete`(deprecated,下个大版本删)。
|
|
201
206
|
- **a11y**:所有可聚焦元素必须有 `:focus-visible` 环;状态变化必须同步 `aria-*`;modal/dialog 必须有 focus trap + ESC 关闭 + 焦点还原。
|
|
202
207
|
- **动效守卫**:所有动画/过渡在 `@media (prefers-reduced-motion: reduce)` 下压缩为 ~0ms;触屏(`pointer: coarse`)关闭 `:hover` 抬升;高对比度模式(`forced-colors: active`)保留焦点环。
|
|
203
208
|
- 新组件 = `src/components/<name>.css` 一个文件 + 文档站 components 页加一节;交互行为放 `src/behaviors/<name>.ts` 并配套 CSS 类契约。每个 CSS 头注释必须列出完整 DOM 契约。
|
|
204
209
|
- behaviors 全部 SSR 守卫(`typeof document === 'undefined'`),文本赋值用 `textContent`(禁 innerHTML),init 函数全部幂等(重复调用安全,通过 WeakSet/MarkedElement.__icen*Init 标记)。initTabs/initNav/initBackTop 等返回**销毁函数**(移除监听 + 复位幂等标记,销毁后可重新 init)。
|
|
205
|
-
- 函数式组件族(charts / datatable
|
|
206
|
-
- **图表按需安装**:charts 细分为 11 个独立可安装的 kit 入口(`chart-line` / `chart-bar` / `chart-pie` / `chart-radar` / `chart-heatmap` / `chart-area` / `chart-stack` / `chart-gauge` / `chart-sparkline` / `chart-scatter` / `chart-calendar`)+ umbrella `charts`(一条 import 全拿)。**通用层(v0.8)**:`renderChart(el, spec)` 单一入口吃纯 JSON ChartSpec(type 别名归一 `normalizeChartSpec` / 自动选型 `inferChartType` / `data[]+dims` 任意维度 pivot / `format` 描述符替代回调——AI 调用层的直接底座);交互委托 `icen:chart-hover(enter/move/leave)/click/dblclick/contextmenu`(detail 含 index/seriesIndex/seriesName/label/value/指针坐标,右键默认 preventDefault)+ 内置 tooltip(portal,`tooltip:false` 关)+ 图例点击切换系列(`icen:chart-legend-toggle`);图例可见性统一由 renderChart 头部行(.chart-head = 标题 + 小眼睛)控制——根 `data-legend-hidden` 容器级 CSS 隐藏全部图型图例(`spec.legend=false` 定初值,事件 `icen:chart-legend-visibility`),图表体渲染进内层 .chart-body(底层渲染器按契约清空传入 el,头部不能同层);多系列 line(调色盘)/ vbar(分组 `stacked` 堆叠)已内建,标记统一 `data-chart-mark` + `data-chart-*` 数据位。共享基座在 `charts.css`(.chart / .chart-legend / .chart-labels / 调色盘),各类型专属样式在独立 `chart-<type>.css`;所有渲染函数仍在 `behaviors/charts.ts` 一个文件(tree-shake
|
|
210
|
+
- 函数式组件族(charts / datatable / ai 族 render*):不写类契约,导出 `createX(el, opts) → handle`;render* 一律返回挂载元素(有生命周期的图表返回 ChartHandle)——「有生命周期返 handle、静态返容器元素」,写文档示例时勿再写 void。同样一文件 CSS + 文档页一节 + slugs.mjs 登记。datatable 右键菜单复用 context-menu,其依赖的额外 CSS 走 slugs.mjs 的 EXTRA_CSS(kit 入口自动带上)。
|
|
211
|
+
- **图表按需安装**:charts 细分为 11 个独立可安装的 kit 入口(`chart-line` / `chart-bar` / `chart-pie` / `chart-radar` / `chart-heatmap` / `chart-area` / `chart-stack` / `chart-gauge` / `chart-sparkline` / `chart-scatter` / `chart-calendar`)+ umbrella `charts`(一条 import 全拿)。**通用层(v0.8)**:`renderChart(el, spec)` 单一入口吃纯 JSON ChartSpec(type 别名归一 `normalizeChartSpec` / 自动选型 `inferChartType` / `data[]+dims` 任意维度 pivot / `format` 描述符替代回调——AI 调用层的直接底座);交互委托 `icen:chart-hover(enter/move/leave)/click/dblclick/contextmenu`(detail 含 index/seriesIndex/seriesName/label/value/指针坐标,右键默认 preventDefault)+ 内置 tooltip(portal,`tooltip:false` 关)+ 图例点击切换系列(`icen:chart-legend-toggle`);图例可见性统一由 renderChart 头部行(.chart-head = 标题 + 小眼睛)控制——根 `data-legend-hidden` 容器级 CSS 隐藏全部图型图例(`spec.legend=false` 定初值,事件 `icen:chart-legend-visibility`),图表体渲染进内层 .chart-body(底层渲染器按契约清空传入 el,头部不能同层);多系列 line(调色盘)/ vbar(分组 `stacked` 堆叠)已内建,标记统一 `data-chart-mark` + `data-chart-*` 数据位。共享基座在 `charts.css`(.chart / .chart-legend / .chart-labels / 调色盘),各类型专属样式在独立 `chart-<type>.css`;所有渲染函数仍在 `behaviors/charts.ts` 一个文件(tree-shake 友好);调色盘扩展口 `registerChartTone(name, cssVar)`(注册第 7+ 档自定义 tone,主题层/消费方用,内置六档之外时必走它)。子类型 slug → charts 行为模块的映射走 slugs.mjs 的 **`SLUG_BEHAVIOR`**(key=子类型 slug,value='charts'),kit 入口按 `SLUG_EXPORTS` 只 re-export 该类型的渲染函数。
|
|
207
212
|
- 组件 slug 映射(css 合并文件 / init 函数名 / behavior 模块名)的单一事实源是 `scripts/slugs.mjs`——新组件必须在此登记;`scripts/build-css.ts` 据此生成 kit 一行入口(`dist/components/<slug>.mjs` = import css + re-export behavior,消费侧 `@icen.ai/ui/kit/<slug>`)与 registry.json 的 slugs 面,CLI(`dist/cli.mjs`,`bunx @icen.ai/ui add <slug>` 打印引入行,不拷贝源码)与文档站都消费它。`slugs.mjs` 的类型声明在 `slugs.d.mts`,加导出成员时两处同步改。behavior 与 slug 不同名的(如 segmented 的 OTP 由 input.ts 的 initInput 提供)在 `SLUG_BEHAVIOR` 登记映射、在 `SLUG_EXPORTS` 登记 re-export,kit 入口才能正确带出。
|
|
208
213
|
- **kit 的 Astro 陷阱**:kit 入口里的 css import 在 Astro 页面 `<script>`(client bundle)里会被摇掉——Astro 项目必须在布局 frontmatter 里引组件 CSS(accounts 已踩过);Vite SPA / webpack 无此问题。文档站安装段已带此提示。
|
|
209
|
-
-
|
|
214
|
+
- **文档站客户端换页(ClientRouter)**:Docs 布局挂 `<ClientRouter prefetch />`,组件↔原语↔色彩↔各组件页之间为客户端换页(首页走 Base 布局,进出仍整页加载)。两条硬约定:① 页头/页脚 `transition:persist` 常驻,页头脚本经 `astro:after-swap` 对齐新页 active 并滑移导航墨条;② **页面级模块脚本(同 src 只评估一次)一律 `astro:page-load` 驱动**(首载 + 每次换页都触发),全局监听(onIcen 等)重入前先退订上一代;Docs 侧栏内联脚本用 `data-astro-rerun` 强制重跑(组开合记忆存 window 内存——客户端换页保留手动展开、真刷新归零回「全收 + 深链开组」)。新增页面脚本必须遵守,否则换页后接线断线。
|
|
215
|
+
- 文档站(`site/`):站壳/代码块样式集中在 `site/src/styles/site.css`;可复用展示件在 `site/src/components/`(SiteHeader / SiteFooter / CodeBlock / PmInstall / ThemeRestore 防闪烁);轻量语法高亮在 `site/src/lib/highlight.ts`(单行 token 化,`.tok-*` 颜色消费语义 token)。站顶 ⌘K 全站搜索 = 自家 command-palette 组件直出(条目由 `COMPONENTS` 生成),既是功能也是门面条 demo。
|
|
210
216
|
- **skill `icen-ui` 源在本仓库**(`skill/icen-ui/`,SKILL.md + 中英产品页):只写 WHEN/WHICH 场景配方,不抄 API 签名(事实层在 AGENTS.md/.d.mts)。改完跑 `bun scripts/sync-skill.mjs` 单向同步到兄弟仓库 `../skill/skills/icen-ui/`(skill 仓库那份是产物,勿手改),在该仓库提交 push 后 CI 自动上 skill.icen.ai;`--check` 可挂 CI 防漂移。
|
|
211
217
|
- 文档站字体体系(site.css 顶部):prose/站壳 = `--site-font-sans`(系统无衬线),demo 区 = `--site-font-mono`(Plex Mono + CJK 无衬线回退),展示级大标题才用 `--font-heading` 宋体——小字号中文一律不走宋体/通用 monospace 回退。
|
|
212
218
|
- CSS 产物由 `scripts/build-css.ts` 生成,**不要手改 dist/**。`tokensExtras`(如 retro-effects.css)单独拷贝、暴露 exports,**不**进 `tokens.css`/`ui.css` 默认拼合。
|
|
213
219
|
- clay 的 12 基值 token 改动属于品牌级变更;新增色彩预设 = 在 colors.css 加 `.<name>` + `.<name>.dark` 两块完整 token 面(仿照现有 6 套),并注册到 `src/behaviors/theme.ts` 的 PRESETS 与文档站。
|
|
214
220
|
- 文档分组(`site/src/lib/components.ts` 的 `GROUPS`)= 侧栏顺序 = 索引页分组顺序:基础 / AI 原生 / 表单 / 数据展示 / 图表 / 浮层 / 反馈 / 导航(v0.8 调整:AI 上移第二位做拉新门面;「表格」并入数据展示;AI 组更名「AI 原生」)。侧栏分组为 `<details>` 折叠(可多开,首访全开,localStorage `icen.docs.nav.groups` 记忆,深链自动展开当前组)。
|
|
215
|
-
- **反馈三件正交**:`toast`(瞬时 2.6s 右下角)/ `alert`(内嵌页面流)/ `notification`(持久右上角栈,工程级——进度通知/confirm Promise/多按钮/hover 暂停/倒计时/优先级置顶/已读未读/持久化/多容器 createNotificationCenter)——三者各司其职不互相替代;`copy` 是原地按钮反馈(区别于 toast 的全局通知)。
|
|
221
|
+
- **反馈三件正交**:`toast`(瞬时 2.6s 右下角)/ `alert`(内嵌页面流)/ `notification`(持久右上角栈,工程级——进度通知/confirm Promise/多按钮/hover 暂停/倒计时/优先级置顶/已读未读/持久化/多容器 createNotificationCenter)——三者各司其职不互相替代;`copy` 是原地按钮反馈(区别于 toast 的全局通知)。JS 词表已统一为 `toast.success/error`(ok/err 为 deprecated 别名,下大版本删)
|
|
216
222
|
- **徽章三件正交**:`pill`(行内状态徽章,语义色)/ `tag`(中性展示标签,可选关闭按钮与选中态)/ `badge`(角标式数字/圆点,挂外层元素角上,外层需 `position: relative`)。
|
|
217
223
|
- **组件 v2 增强(0.5.0 全量升级)**:所有基础组件已达到工程级深度——
|
|
218
224
|
- **select**:`data-select-search` 可搜索过滤(支持 `data-keywords` 辅助关键字 + 分组自动隐藏)、`data-select-multiple` 多选(chips 显示 + `data-select-max` 上限 + `.select-clear` 清除钮)、`.select-group` / `.select-group-label` 分组、`.select-empty` 无结果态。
|
|
@@ -229,7 +235,7 @@ bun run build # tsup(src/behaviors/*.ts → dist/behaviors/*.mjs + index.m
|
|
|
229
235
|
- **tabs**:可关闭 tab(`.page-tab-close`)、徽标(`.page-tab-badge`)、图标(`.page-tab-icon`)、分段式(`--segment`)、垂直(`--vertical`)、尺寸(`--sm/lg`)。
|
|
230
236
|
- **nav**:搜索槽(`.nav-search`)、通知徽标(`.nav-btn-badge`)、链接式(`.nav-link`)、用户区(`.nav-user`)。
|
|
231
237
|
- **sidebar**:头部/底部槽(`.sidebar-header` / `.sidebar-footer`)、折叠/轨道模式(`--collapsed`)、激活指示条、分隔线、徽章变体色。
|
|
232
|
-
- **content**:列表增强(`.list-item-leading/meta/trailing` + 选中/斑马)、时间线变体(右侧/交替/自定义尺寸/扩展内容槽)、描述列表变体(行内/垂直/可复制)、树增强(连接线 `.tree--lined` +
|
|
238
|
+
- **content**:列表增强(`.list-item-leading/meta/trailing` + 选中/斑马)、时间线变体(右侧/交替/自定义尺寸/扩展内容槽)、描述列表变体(行内/垂直/可复制)、树增强(连接线 `.tree--lined` + 搜索高亮)、代码块(`.code-block`)、kbd 大号/按下态。
|
|
233
239
|
- **media**:头像变体(语义色/渐变/方形/通知环)、头像组溢出计数、评分只读/清除/计数/摘要、轮播缩略图/索引/渐入。
|
|
234
240
|
- **toast**:语义图标、关闭钮、富内容(标题+正文)、超时进度条、6 种容器定位。
|
|
235
241
|
- **pill**:可关闭(`.pill-close`)、图标、实时脉冲(`--live`)、纯数字(`--count`)、渐变(`--gradient`)。
|
|
@@ -250,7 +256,7 @@ bun run build # tsup(src/behaviors/*.ts → dist/behaviors/*.mjs + index.m
|
|
|
250
256
|
- **ai-usage 双形态**:分段条 `renderAiUsage` + 上下文窗口环形指示器 `renderAiUsageRing`(点击弹出完整分解,弹层复用 popover;状态档 <60% accent / 60–85% warning / >85% error+脉冲)。配套 `renderAiAudit` 审计面板(totals + byModel + 最近条目,清除钮派 `icen:ai-audit-clear`);`ai-context` 抽屉的 `model.audit` 自动渲染审计节(紧凑形态)。
|
|
251
257
|
- **ai-composer v2**(业界集大成):底部工具条 = 模型切换(多 provider 分组 + 搜索 + 缓存失效提示,选中模型 context 自动喂给环的 total;无 context 显式清除不留陈旧值)+ 上下文环挂载点 + 附件 + 发送/停止;`/` 命令弹层(拦截执行不进消息流);`@` 引用弹层(chip 分离渲染,textarea 不嵌 chip);空输入 `↑` 取回历史,运行中 `↑` 取回排队消息。附件三入口:钮选 / textarea 粘贴文件 / box 拖放(`.is-dragover` 高亮)。API:setComposerModels / setComposerCommands / setComposerRefs / setComposerUsage / bindComposer。
|
|
252
258
|
- **绑定层 `bindComposer`**(v0.7.1,spec §11,「零接线全链路」):事件驱动把 composer ↔ 消息区(renderAiMessage)↔ client(stream + 停止 + 排队自动续发 + 错误 → setError/fail)↔ 用量环(`usage.from: 'context'` 默认正确口径 | `'billing'` | AiAuditor)接成闭环;运行态自动管理(send→running / icen:ai-done→解除);不传 client 为纯状态绑定(渐进采用);重复绑定 WeakSet 防护,unbind 解绑。
|
|
253
|
-
- **ai-provider 适配层**(`src/behaviors/ai-provider.ts`,无 UI 不占 slug):注册表内置 openai/claude/deepseek/glm/kimi
|
|
254
|
-
- **AI 工具体系(v0.8,`src/behaviors/ai-tools.ts`,不绑定 slug)**:UI 能力注册为模型可调用的 AiToolDef(内置 render_chart,inputSchema=ChartSpec);createAiToolArea 挂载区三层控制(不 import=无 / 白名单+max
|
|
259
|
+
- **ai-provider 适配层**(`src/behaviors/ai-provider.ts`,无 UI 不占 slug):注册表内置 openai/claude/deepseek/glm/kimi 五家(`registerAiProvider`/`getAiProvider` 注册与读取第三方/自建网关;baseURL 可自定义,models 携带定价表——`estimateCost` 未命中返回 undefined 不猜价),`createAiClient` 填 key 即用(chat + stream 双族线协议,多模态 parts 传输:openai 族 image_url/input_audio/file_id、anthropic 族 image/audio/document(pdf) + `cacheControl` 断点 + role 'tool' 的 tool_result 回灌;video 两族诚实报 unsupported),usage 走 normalizeUsage 归一(含各家缓存字段),`createAiAuditor` 环形审计(条目含 cost 定价估算 + ttftMs 首 token 延迟;`summary()` 喂环 / `totals()` 喂审计面板 / `byModel()` 分组),每次请求派 `icen:ai-usage` 与 `icen:ai-done` 事件。浏览器直连:claude 官方支持(自动带直连头),openai 官方禁止,其余三家建议代理(browserDirect 字段诚实标注)。
|
|
260
|
+
- **AI 工具体系(v0.8,`src/behaviors/ai-tools.ts`,不绑定 slug)**:UI 能力注册为模型可调用的 AiToolDef(内置 render_chart,inputSchema=ChartSpec);createAiToolArea 挂载区三层控制(不 import=无 / 白名单+max FIFO 淘汰最旧挂载 / 运行时 setTools·setMax·clear);`AiToolDef.present: 'mount'|'data'`(data=不占挂载位,记录与事件照常,run 返回值即结果);注册面 `registerAiTool`·`unregisterAiTool`(联动摘除各区域已挂载卡)·`getAiTool`·`listAiTools`;模型侧导出 aiToolsToOpenAI/aiToolsToMcp/aiToolsManifest + parseAiToolArgs;工具回执内嵌图(**仅 output**;input 是机器参数保持 JSON)→ renderAiToolCall IO 区直接 renderChart;待办业界模式:TodoWrite 折叠卡(todo kind summarize x/y)+ `setComposerTodo` 输入区实时 chip(全完成/有失败图标反馈,点击弹层看清单);事件 icen:ai-tool-call/-result/-evict。规格 §13。
|
|
255
261
|
- **ai-core 不绑定任何 slug**:demo/消费方要用 `normalizeToolCall` 等时,在该页 behaviors 数组里显式加 `'ai-core'`(boot 的 glob 会加载并注入 `aiCoreMod` 参数)。
|
|
256
262
|
- **宽度自稳定契约**:所有 AI 输出容器根(`.ai-chat/.ai-reasoning/.ai-composer/.ai-item/.ai-tool/.ai-subagent/.ai-diff/.ai-files/.ai-todo/.ai-usage`)声明 `align-self: stretch`——在 flex/grid 居中宿主(shrink-wrap 上下文,如文档站 `.demo-stage`)里不随内容多少改变宽度,流式内容只允许纵向生长。**禁用 `width: 100%`** 做这件事(在 shrink-wrap 父级下会塌成 min-content)。消费方给组件包一层自己的布局 wrapper 时,wrapper 的宽度由消费方负责(demo 里直接 `style="width:100%"`)。
|
package/README.md
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# @icen.ai/ui — Icen Design System
|
|
2
2
|
|
|
3
|
-
icen.ai 全生态的统一设计系统:**设计 tokens(6 色彩预设 × 明暗双模 × 3 风格 profile)+ 无框架组件 CSS(56 组件)+ 行为 JS(
|
|
3
|
+
icen.ai 全生态的统一设计系统:**设计 tokens(6 色彩预设 × 明暗双模 × 3 风格 profile)+ 无框架组件 CSS(56 组件)+ 行为 JS(35 behaviors)+ 统一事件体系 + AI 原生组件族 + 图表通用层 + AI 工具体系**。工程形态与 `@icen.ai/cli` 一致(Bun + TS + tsup + ESM + MIT)。
|
|
4
4
|
|
|
5
5
|
- 色彩预设:`clay`(默认,陶土橙 × 纸白 = icen 品牌)/ `piano` / `art` / `vangogh` / `ink` / `retro`,各含 `.dark` 变体
|
|
6
6
|
- 风格配置:`.style-modern` / `.style-retro` / `.style-terminal`(几何 / 密度 / 动效 / 字体 token,与色彩正交)
|
|
7
7
|
- 可选效果层:`retro-effects.css`(CRT 扫描线 / 颗粒 / 像素边框 / 打字机光标 —— 仅 `.style-retro` 激活时生效,按需 import)
|
|
8
|
-
- 组件:btn / form / input / select / slider / switch / tag-input / upload / modal / popover / tooltip / menu(含 dropdown + context-menu)/ tabs / nav / sidebar / breadcrumb / pagination / steps / segmented / accordion / tree / carousel / charts / table / stat / card / panel / pill / tag / badge / empty / feedback / content / media / toolbar / split-pane / date-picker / command-palette / notification / copy / scroll-area / back-top / datatable / layout(56 个组件 CSS,含 charts 基座 + 11 个细分图表 kit
|
|
8
|
+
- 组件:btn / form / input / select / slider / switch / tag-input / upload / modal / popover / tooltip / menu(含 dropdown + context-menu)/ tabs / nav / sidebar / breadcrumb / pagination / steps / segmented / accordion / tree / carousel / charts / table / stat / card / panel / pill / tag / badge / empty / feedback / content / media / toolbar / split-pane / date-picker / command-palette / notification / copy / scroll-area / back-top / datatable / layout(56 个组件 CSS,含 charts 基座 + 11 个细分图表 kit,贡献日历样式并入基座);浮层尺寸统一 PanelSizing 契约(`data-panel-width/-min/-max/-min-height/-max-height` 属性面 + 程序面对象,含视口夹取)
|
|
9
9
|
- 图表通用层(v0.8):`renderChart(el, spec)` 统一入口——纯 JSON 规格(type 缺省自动推断;data[] + dims 任意维度透视)、多系列折线/分组堆叠柱、内置 tooltip、`icen:chart-hover/click/dblclick/contextmenu` 交互事件族、图例点击切换系列;新增贡献日历 renderCalendar(GitHub 同款)与散点气泡 renderScatter
|
|
10
10
|
- AI 原生组件族(v0.7,11 个 slug):ai-chat(会话容器)/ ai-message(消息行)/ ai-reasoning(推理块)/ ai-composer(输入台)/ ai-tool-call(工具调用卡)/ ai-subagent(子智能体卡)/ ai-diff(差异审阅)/ ai-files(文件标签)/ ai-todo(任务清单)/ ai-context(上下文抽屉)/ ai-usage(用量条)——共享 7 态状态机与 kind 注册表,事件统一 `icen:ai-*` 前缀
|
|
11
11
|
- 标准化内容模型 `AiContent`(v0.7.1,对标 AI SDK v5 parts / MCP / OpenAI / Anthropic,2026-10 调研):一套 `AiContentPart[]`(text / image / audio / video / file / resource-link)表达任意输入——消息渲染(renderAiMessage 多模态 + 错误变体)、工具回执(MCP content 数组零改动进卡片)、传输层(parts → 两族 wire,含 anthropic `cache_control` 与 tool_result 回灌)全部只认这一套;`normalizeContentParts` 一函数归一四族来源,`estimateTokens` / `contextEstimate` 提供粗估与上下文口径
|
|
12
12
|
- ai-provider 适配层(v0.7.1 增强):五家厂商注册表(含定价表,`estimateCost` 未命中不猜价)、chat + stream 双族线协议多模态传输、审计闭环(`createAiAuditor` 条目含 cost 定价估算与 ttftMs 首 token 延迟,`renderAiAudit` 面板 + `ai-context` 抽屉审计节)、请求收尾派 `icen:ai-done`(status/usage/cost/error/durationMs/ttftMs)
|
|
13
13
|
- 绑定层 `bindComposer`(v0.7.1,「零接线全链路」):一行把 composer ↔ 消息区(renderAiMessage)↔ client(stream + 停止 + 排队续发 + 错误路径)↔ 上下文环(`usage.from: 'context'` 正确口径 / `'billing'` 计费口径 / auditor)接成闭环;不传 client 为纯状态绑定(渐进采用);附件三入口(钮选 / 粘贴 / 拖放)
|
|
14
14
|
- AI 工具体系(v0.8,`ai-tools`):UI 能力注册为模型可调用的工具(内置 `render_chart` 吃 ChartSpec 纯 JSON);`createAiToolArea` 挂载区三层控制(白名单 / max LRU / 运行时调节);模型侧 `aiToolsToOpenAI()` 直接给 tools 参数、`aiToolsManifest()` 贴 system prompt;工具回执内嵌图(output 为 chart spec → 工具卡展开区直接渲染)
|
|
15
|
-
-
|
|
15
|
+
- 事件体系(events):`onIcen` 一个入口(类型化 `IcenEventMap` + 选择器委托 + `{ within, signal, once }`)+ `emitIcen` 唯一派生口 + `setEventPolicy` 手势默认行为 + `data-gestures` 声明式手势(click/dblclick/contextmenu/longpress/text-select,位移守卫与消歧内建)
|
|
16
|
+
- 控件族(controls):`initSwitch`(开关/三态勾选/单选组)/ `initStepper` / `initSegmented` / `initSteps` / `initRating` / `initPagination` / `initTableSort`——纯 CSS 控件的行为承载与 `icen:*-change` 事件
|
|
17
|
+
- 行为:theme / tabs / toast / copy / input / select / slider / tag-input / upload / modal / dropdown / popover / context-menu / accordion / tree / carousel / charts / nav / sidebar / datatable / notification / back-top / command-palette / date-picker / split-pane / events / controls / ai-core / ai-chat / ai-composer / ai-tool / ai-provider / ai-diff / ai-panel / ai-tools(35 个,全部 SSR 守卫 + 幂等 init + 返回销毁函数 + textContent-only)
|
|
16
18
|
|
|
17
19
|
## 使用
|
|
18
20
|
|
|
@@ -43,6 +45,9 @@ import {
|
|
|
43
45
|
} from '@icen.ai/ui';
|
|
44
46
|
|
|
45
47
|
initTheme(); // 尽早调用(或内联同步脚本防闪烁,见 accounts Base.astro)
|
|
48
|
+
setStyle('retro'); // 切风格画像(modern/retro),与主题正交
|
|
49
|
+
registerThemePreset('brand'); // 登记自建预设(宿主 CSS 先写好 .brand/.brand.dark token 面)
|
|
50
|
+
listThemePresets(); // 枚举全部预设(含注册的),UI 切换器用
|
|
46
51
|
initTabs(); // [data-tabs] > [data-tab] + [data-tab-panel] 契约;切换派发 icen:tab-change
|
|
47
52
|
initCopy(); // .copy-btn[data-copy] 委托
|
|
48
53
|
toast.ok('已保存'); // 或 toast.action('已删除', '撤销', undo);类名 .toast--success 等
|
package/dist/base.css
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* ════════════════════════════════════════════════════════════════════════
|
|
8
8
|
* icen 组件契约(统一抽象)
|
|
9
9
|
* ════════════════════════════════════════════════════════════════════════
|
|
10
|
-
* 所有
|
|
10
|
+
* 所有 56 个组件遵循同一套抽象,由两层共同支撑:
|
|
11
11
|
*
|
|
12
12
|
* ── 层 1:--icen-* 原语 token(所有组件自洽消费)
|
|
13
13
|
* --icen-ring / --icen-ring-soft / --icen-ring-error /
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
* 暴露的契约:
|
|
56
56
|
* - 元素基线:box-sizing / body / a / code / pre / img / ::selection
|
|
57
57
|
* - 滚动条:贴 --token-scrollbar-*(retro profile 另有硬覆盖)
|
|
58
|
-
* - 工具类:.mono / .dim / .faint / .sr-only / .skip-link
|
|
58
|
+
* - 工具类:.mono / .serif / .dim / .faint / .numeric / .sr-only / .skip-link
|
|
59
59
|
* - z-index 标尺:--z-* (浮层一律 portal 到 body)
|
|
60
60
|
* - 共享 keyframes:icen-pop-in(弹层进场)/ icen-spin(旋转)/ icen-skeleton-pulse(骨架脉冲)
|
|
61
61
|
* - 交互基元 class:.pressable / .control / .field / .lift / .surface-elevated / .focus-ring / .motion
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
───────────────────────────────────────────────────────────────
|
|
68
68
|
所有组件消费的"原语级"变量:聚焦环、悬停、按下、禁用、过渡。
|
|
69
69
|
风格 profile(如 .style-retro)只需覆盖这里的少数变量,
|
|
70
|
-
即可让
|
|
70
|
+
即可让 56 个组件整体换肤;组件自身无需感知风格切换。
|
|
71
71
|
══════════════════════════════════════════════════════════════ */
|
|
72
72
|
:root {
|
|
73
73
|
/* ── 聚焦环(focus-visible) ── */
|
|
@@ -391,6 +391,7 @@ html {
|
|
|
391
391
|
<span class="field-label">姓名</span>
|
|
392
392
|
<span class="control"><input /></span>
|
|
393
393
|
<span class="field-hint">必填</span>
|
|
394
|
+
<span class="field-error">错误</span>
|
|
394
395
|
</label> */
|
|
395
396
|
.field {
|
|
396
397
|
display: flex;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 为 root 下每个 .accordion 容器初始化(root 自身是 .accordion 也算)。
|
|
3
|
+
* 返回销毁函数:移除本次挂上的容器监听并复位幂等标记(销毁后可重新 init)。SSR 下返回 no-op。
|
|
4
|
+
*/
|
|
5
|
+
declare function initAccordion(root?: ParentNode): () => void;
|
|
3
6
|
|
|
4
7
|
export { initAccordion };
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
// src/behaviors/events.ts
|
|
2
|
+
function emitIcen(source, type, detail) {
|
|
3
|
+
return source.dispatchEvent(
|
|
4
|
+
new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
|
|
5
|
+
);
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
// src/behaviors/accordion.ts
|
|
2
9
|
function setup(container) {
|
|
3
10
|
const el = container;
|
|
4
|
-
if (el.__icenAccordionInit) return;
|
|
11
|
+
if (el.__icenAccordionInit) return void 0;
|
|
5
12
|
el.__icenAccordionInit = true;
|
|
6
13
|
const single = container.hasAttribute("data-single");
|
|
7
|
-
|
|
14
|
+
const onClick = (ev) => {
|
|
8
15
|
const target = ev.target;
|
|
9
16
|
if (!(target instanceof Element)) return;
|
|
10
17
|
const trigger = target.closest(".accordion-trigger");
|
|
@@ -14,6 +21,8 @@ function setup(container) {
|
|
|
14
21
|
if (item.classList.contains("is-disabled")) return;
|
|
15
22
|
const open = item.classList.toggle("is-open");
|
|
16
23
|
trigger.setAttribute("aria-expanded", String(open));
|
|
24
|
+
const index = Array.from(container.querySelectorAll(".accordion-item")).indexOf(item);
|
|
25
|
+
emitIcen(container, "icen:accordion-toggle", { item, index, open });
|
|
17
26
|
if (single && open) {
|
|
18
27
|
container.querySelectorAll(".accordion-item.is-open").forEach((other) => {
|
|
19
28
|
if (other === item) return;
|
|
@@ -21,8 +30,8 @@ function setup(container) {
|
|
|
21
30
|
other.querySelector(".accordion-trigger")?.setAttribute("aria-expanded", "false");
|
|
22
31
|
});
|
|
23
32
|
}
|
|
24
|
-
}
|
|
25
|
-
|
|
33
|
+
};
|
|
34
|
+
const onKeyDown = (ev) => {
|
|
26
35
|
const kev = ev;
|
|
27
36
|
const target = kev.target;
|
|
28
37
|
if (!(target instanceof Element)) return;
|
|
@@ -40,15 +49,31 @@ function setup(container) {
|
|
|
40
49
|
kev.preventDefault();
|
|
41
50
|
triggers[triggers.length - 1]?.focus();
|
|
42
51
|
}
|
|
43
|
-
}
|
|
52
|
+
};
|
|
53
|
+
container.addEventListener("click", onClick);
|
|
54
|
+
container.addEventListener("keydown", onKeyDown);
|
|
55
|
+
return () => {
|
|
56
|
+
container.removeEventListener("click", onClick);
|
|
57
|
+
container.removeEventListener("keydown", onKeyDown);
|
|
58
|
+
el.__icenAccordionInit = false;
|
|
59
|
+
};
|
|
44
60
|
}
|
|
45
61
|
function initAccordion(root) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
const cleanups = [];
|
|
63
|
+
if (typeof document !== "undefined") {
|
|
64
|
+
const scope = root ?? document;
|
|
65
|
+
const containers = [];
|
|
66
|
+
if (scope instanceof Element && scope.matches(".accordion")) containers.push(scope);
|
|
67
|
+
containers.push(...Array.from(scope.querySelectorAll(".accordion")));
|
|
68
|
+
for (const c of containers) {
|
|
69
|
+
const cleanup = setup(c);
|
|
70
|
+
if (cleanup) cleanups.push(cleanup);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return () => {
|
|
74
|
+
for (const fn of cleanups) fn();
|
|
75
|
+
cleanups.length = 0;
|
|
76
|
+
};
|
|
52
77
|
}
|
|
53
78
|
export {
|
|
54
79
|
initAccordion
|
|
@@ -45,7 +45,9 @@ interface AiMessageHandle {
|
|
|
45
45
|
stream(): AiStreamHandle;
|
|
46
46
|
}
|
|
47
47
|
/** 单个内容部件 → DOM 节点(文本一律 textContent;媒体 src 走 aiContentUrl 归一)。
|
|
48
|
-
* 导出供 ai-tool 的 IO 区复用(MCP 多模态回执;类名 .ai-msg-* 语义即「消息部件」,全局生效)。
|
|
48
|
+
* 导出供 ai-tool 的 IO 区复用(MCP 多模态回执;类名 .ai-msg-* 语义即「消息部件」,全局生效)。
|
|
49
|
+
* 前置条件:浏览器环境——本函数无 SSR 守卫,SSR 由调用方守卫覆盖(renderAiMessage /
|
|
50
|
+
* renderAiToolCall 头部守卫后才会触达此处)。 */
|
|
49
51
|
declare function renderAiContentPart(part: AiContentPart): HTMLElement | undefined;
|
|
50
52
|
/**
|
|
51
53
|
* DOM API 渲染一条消息(append 到容器末尾):多角色变体 + 多模态部件 + copy/retry
|
|
@@ -135,9 +135,9 @@ function formatDuration(ms) {
|
|
|
135
135
|
const m = Math.floor(ms / 6e4);
|
|
136
136
|
const s = Math.round(ms % 6e4 / 1e3);
|
|
137
137
|
if (ms < 36e5) return s > 0 ? `${m}m${s}s` : `${m}m`;
|
|
138
|
-
const
|
|
138
|
+
const h2 = Math.floor(ms / 36e5);
|
|
139
139
|
const rm = Math.round(ms % 36e5 / 6e4);
|
|
140
|
-
return rm > 0 ? `${
|
|
140
|
+
return rm > 0 ? `${h2}h${rm}m` : `${h2}h`;
|
|
141
141
|
}
|
|
142
142
|
function aiContentUrl(part) {
|
|
143
143
|
if (part.type === "text" || part.type === "resource-link") return void 0;
|
|
@@ -147,6 +147,12 @@ function aiContentUrl(part) {
|
|
|
147
147
|
}
|
|
148
148
|
return void 0;
|
|
149
149
|
}
|
|
150
|
+
function h(tag, className, text) {
|
|
151
|
+
const node = document.createElement(tag);
|
|
152
|
+
if (className) node.className = className;
|
|
153
|
+
if (text != null) node.textContent = text;
|
|
154
|
+
return node;
|
|
155
|
+
}
|
|
150
156
|
function svgIcon(svg2) {
|
|
151
157
|
if (typeof document === "undefined" || typeof DOMParser === "undefined") return null;
|
|
152
158
|
let parsed;
|
|
@@ -159,15 +165,15 @@ function svgIcon(svg2) {
|
|
|
159
165
|
if (!root || root.tagName.toLowerCase() !== "svg" || parsed.querySelector("parsererror")) {
|
|
160
166
|
return null;
|
|
161
167
|
}
|
|
162
|
-
const sanitize = (
|
|
163
|
-
for (const attr of Array.from(
|
|
168
|
+
const sanitize = (el) => {
|
|
169
|
+
for (const attr of Array.from(el.attributes)) {
|
|
164
170
|
const name = attr.name.toLowerCase();
|
|
165
|
-
if (name.startsWith("on"))
|
|
171
|
+
if (name.startsWith("on")) el.removeAttribute(attr.name);
|
|
166
172
|
if ((name === "href" || name === "xlink:href") && attr.value.trim().toLowerCase().startsWith("javascript:")) {
|
|
167
|
-
|
|
173
|
+
el.removeAttribute(attr.name);
|
|
168
174
|
}
|
|
169
175
|
}
|
|
170
|
-
for (const child of Array.from(
|
|
176
|
+
for (const child of Array.from(el.children)) {
|
|
171
177
|
const tag = child.tagName.toLowerCase();
|
|
172
178
|
if (tag === "script" || tag === "foreignobject") {
|
|
173
179
|
child.remove();
|
|
@@ -180,6 +186,13 @@ function svgIcon(svg2) {
|
|
|
180
186
|
return document.importNode(root, true);
|
|
181
187
|
}
|
|
182
188
|
|
|
189
|
+
// src/behaviors/events.ts
|
|
190
|
+
function emitIcen(source, type, detail) {
|
|
191
|
+
return source.dispatchEvent(
|
|
192
|
+
new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
183
196
|
// src/behaviors/ai-chat.ts
|
|
184
197
|
var ICON_COPY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="2" width="8" height="4" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg>';
|
|
185
198
|
var ICON_RETRY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>';
|
|
@@ -191,7 +204,7 @@ function prefersReducedMotion() {
|
|
|
191
204
|
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
192
205
|
}
|
|
193
206
|
function emit(target, name, detail) {
|
|
194
|
-
target
|
|
207
|
+
emitIcen(target, name, detail);
|
|
195
208
|
}
|
|
196
209
|
function fallbackCopy(text) {
|
|
197
210
|
const ta = document.createElement("textarea");
|
|
@@ -214,9 +227,9 @@ function copyText(text) {
|
|
|
214
227
|
}
|
|
215
228
|
}
|
|
216
229
|
function setupChat(chat) {
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
219
|
-
|
|
230
|
+
const el = chat;
|
|
231
|
+
if (el.__icenAiChatInit) return void 0;
|
|
232
|
+
el.__icenAiChatInit = true;
|
|
220
233
|
const scrollEl = chat.querySelector(".ai-chat-scroll");
|
|
221
234
|
let jump = chat.querySelector(".ai-chat-jump") ?? null;
|
|
222
235
|
let pinned = true;
|
|
@@ -253,7 +266,7 @@ function setupChat(chat) {
|
|
|
253
266
|
} else {
|
|
254
267
|
unread = 0;
|
|
255
268
|
}
|
|
256
|
-
lastMsgCount =
|
|
269
|
+
lastMsgCount = count;
|
|
257
270
|
updateJump();
|
|
258
271
|
}
|
|
259
272
|
};
|
|
@@ -289,7 +302,14 @@ function setupChat(chat) {
|
|
|
289
302
|
if (!msg) return;
|
|
290
303
|
const action = actionBtn.dataset.aiMsgAction;
|
|
291
304
|
if (action === "copy") {
|
|
292
|
-
|
|
305
|
+
const bodyEl = msg.querySelector(".ai-msg-body");
|
|
306
|
+
const clone = bodyEl?.cloneNode(true);
|
|
307
|
+
if (clone instanceof HTMLElement) {
|
|
308
|
+
clone.querySelectorAll(".ai-msg-model").forEach((chip) => chip.remove());
|
|
309
|
+
copyText(clone.textContent ?? "");
|
|
310
|
+
} else {
|
|
311
|
+
copyText("");
|
|
312
|
+
}
|
|
293
313
|
emit(msg, "icen:ai-copy", { el: msg });
|
|
294
314
|
} else if (action === "retry") {
|
|
295
315
|
emit(msg, "icen:ai-retry", { el: msg });
|
|
@@ -353,7 +373,7 @@ function setupChat(chat) {
|
|
|
353
373
|
disposers.push(() => chat.removeEventListener("click", onClick));
|
|
354
374
|
return () => {
|
|
355
375
|
for (const dispose of disposers) dispose();
|
|
356
|
-
|
|
376
|
+
el.__icenAiChatInit = false;
|
|
357
377
|
};
|
|
358
378
|
}
|
|
359
379
|
function initAiChat(root) {
|
|
@@ -375,18 +395,18 @@ function initAiChat(root) {
|
|
|
375
395
|
cleanups.length = 0;
|
|
376
396
|
};
|
|
377
397
|
}
|
|
378
|
-
function createAiStream(
|
|
379
|
-
const reasoning =
|
|
398
|
+
function createAiStream(el) {
|
|
399
|
+
const reasoning = el.closest(".ai-reasoning");
|
|
380
400
|
const startedAt = Date.now();
|
|
381
401
|
let state = "streaming";
|
|
382
|
-
|
|
383
|
-
|
|
402
|
+
el.classList.add("is-streaming");
|
|
403
|
+
el.setAttribute("aria-busy", "true");
|
|
384
404
|
reasoning?.classList.add("is-streaming");
|
|
385
405
|
const settle = (finished) => {
|
|
386
406
|
if (state !== "streaming") return;
|
|
387
407
|
state = finished ? "done" : "cancelled";
|
|
388
|
-
|
|
389
|
-
|
|
408
|
+
el.classList.remove("is-streaming");
|
|
409
|
+
el.removeAttribute("aria-busy");
|
|
390
410
|
if (!reasoning) return;
|
|
391
411
|
reasoning.classList.remove("is-streaming");
|
|
392
412
|
if (!finished) return;
|
|
@@ -400,7 +420,7 @@ function createAiStream(el2) {
|
|
|
400
420
|
return {
|
|
401
421
|
append(text) {
|
|
402
422
|
if (state !== "streaming" || !text) return;
|
|
403
|
-
|
|
423
|
+
el.textContent += text;
|
|
404
424
|
},
|
|
405
425
|
done() {
|
|
406
426
|
settle(true);
|
|
@@ -411,9 +431,9 @@ function createAiStream(el2) {
|
|
|
411
431
|
fail() {
|
|
412
432
|
if (state !== "streaming") return;
|
|
413
433
|
state = "error";
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
434
|
+
el.classList.remove("is-streaming");
|
|
435
|
+
el.removeAttribute("aria-busy");
|
|
436
|
+
el.classList.add("is-error");
|
|
417
437
|
reasoning?.classList.remove("is-streaming");
|
|
418
438
|
}
|
|
419
439
|
};
|
|
@@ -429,15 +449,9 @@ function safeHref(uri) {
|
|
|
429
449
|
if (s.startsWith("javascript:") || s.startsWith("data:text/html")) return void 0;
|
|
430
450
|
return uri;
|
|
431
451
|
}
|
|
432
|
-
function el(tag, className, text) {
|
|
433
|
-
const node = document.createElement(tag);
|
|
434
|
-
if (className) node.className = className;
|
|
435
|
-
if (text != null) node.textContent = text;
|
|
436
|
-
return node;
|
|
437
|
-
}
|
|
438
452
|
function renderAiContentPart(part) {
|
|
439
453
|
if (part.type === "text") {
|
|
440
|
-
const span =
|
|
454
|
+
const span = h("span", "ai-msg-text");
|
|
441
455
|
span.textContent = part.text;
|
|
442
456
|
if (part.state === "streaming") span.classList.add("is-streaming");
|
|
443
457
|
return span;
|
|
@@ -472,8 +486,8 @@ function renderAiContentPart(part) {
|
|
|
472
486
|
return video;
|
|
473
487
|
}
|
|
474
488
|
if (part.type === "file") {
|
|
475
|
-
const chip =
|
|
476
|
-
chip.appendChild(
|
|
489
|
+
const chip = h("span", "ai-msg-file");
|
|
490
|
+
chip.appendChild(h("span", "ai-msg-file-name", part.filename ?? "\u6587\u4EF6"));
|
|
477
491
|
const href2 = part.url ? safeHref(part.url) : void 0;
|
|
478
492
|
if (href2 && /^(https?:|data:)/i.test(href2)) {
|
|
479
493
|
const a2 = document.createElement("a");
|
|
@@ -494,9 +508,18 @@ function renderAiContentPart(part) {
|
|
|
494
508
|
return a;
|
|
495
509
|
}
|
|
496
510
|
function renderAiMessage(scrollEl, model) {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
511
|
+
if (typeof document === "undefined") {
|
|
512
|
+
return {
|
|
513
|
+
el: scrollEl,
|
|
514
|
+
body: scrollEl,
|
|
515
|
+
setMeta: () => void 0,
|
|
516
|
+
setError: () => void 0,
|
|
517
|
+
stream: () => createAiStream(scrollEl)
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
const row = h("div", `ai-msg ai-msg--${model.role}`);
|
|
521
|
+
const avatar = h("span", "ai-msg-avatar", ROLE_AVATAR[model.role] ?? "");
|
|
522
|
+
const body = h("div", "ai-msg-body");
|
|
500
523
|
row.appendChild(avatar);
|
|
501
524
|
row.appendChild(body);
|
|
502
525
|
const parts = typeof model.content === "string" ? [{ type: "text", text: model.content }] : model.content;
|
|
@@ -504,18 +527,18 @@ function renderAiMessage(scrollEl, model) {
|
|
|
504
527
|
const node = renderAiContentPart(part);
|
|
505
528
|
if (node) body.appendChild(node);
|
|
506
529
|
}
|
|
507
|
-
if (model.model) body.appendChild(
|
|
508
|
-
const metaEl =
|
|
530
|
+
if (model.model) body.appendChild(h("span", "ai-msg-model", model.model));
|
|
531
|
+
const metaEl = h("div", "ai-msg-meta");
|
|
509
532
|
if (model.meta != null) metaEl.textContent = model.meta;
|
|
510
533
|
row.appendChild(metaEl);
|
|
511
|
-
const actions =
|
|
512
|
-
const copyBtn =
|
|
534
|
+
const actions = h("div", "ai-msg-actions");
|
|
535
|
+
const copyBtn = h("button", "ai-msg-action");
|
|
513
536
|
copyBtn.type = "button";
|
|
514
537
|
copyBtn.setAttribute("data-ai-msg-action", "copy");
|
|
515
538
|
copyBtn.setAttribute("aria-label", "\u590D\u5236");
|
|
516
539
|
const copyIcon = svgIcon(ICON_COPY);
|
|
517
540
|
if (copyIcon) copyBtn.appendChild(copyIcon);
|
|
518
|
-
const retryBtn =
|
|
541
|
+
const retryBtn = h("button", "ai-msg-action");
|
|
519
542
|
retryBtn.type = "button";
|
|
520
543
|
retryBtn.setAttribute("data-ai-msg-action", "retry");
|
|
521
544
|
retryBtn.setAttribute("aria-label", "\u91CD\u8BD5");
|
|
@@ -533,13 +556,13 @@ function renderAiMessage(scrollEl, model) {
|
|
|
533
556
|
row.classList.add("ai-msg--error");
|
|
534
557
|
let errEl = row.querySelector(".ai-msg-error-text");
|
|
535
558
|
if (!errEl) {
|
|
536
|
-
errEl =
|
|
559
|
+
errEl = h("div", "ai-msg-error-text");
|
|
537
560
|
body.appendChild(errEl);
|
|
538
561
|
}
|
|
539
562
|
errEl.textContent = message;
|
|
540
563
|
},
|
|
541
564
|
stream() {
|
|
542
|
-
const span =
|
|
565
|
+
const span = h("span", "ai-msg-stream-text");
|
|
543
566
|
body.appendChild(span);
|
|
544
567
|
return createAiStream(span);
|
|
545
568
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AiUsage } from './ai-core.js';
|
|
2
|
+
import { PanelSizing } from './popover.js';
|
|
2
3
|
import { AiTodoItem, AiUsageRingOpts } from './ai-panel.js';
|
|
3
4
|
import { AiClient, AiAuditor } from './ai-provider.js';
|
|
4
5
|
|
|
@@ -25,13 +26,29 @@ interface AiComposerCommand {
|
|
|
25
26
|
/** 参数提示(如 "<path>"),弹层命令行右侧灰色展示 */
|
|
26
27
|
argsHint?: string;
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
+
/** 引用 kind:内置 4 种 + 开放注册;`string & {}` 保自动补全同时放行 registerRefKind 注册的任意 kind(照 charts.ts ChartTone 手法) */
|
|
30
|
+
type AiComposerRefKind = 'file' | 'folder' | 'doc' | 'agent' | (string & {});
|
|
29
31
|
interface AiComposerRefSource {
|
|
30
32
|
kind: AiComposerRefKind;
|
|
31
33
|
id: string;
|
|
32
34
|
label: string;
|
|
33
35
|
sub?: string;
|
|
34
36
|
}
|
|
37
|
+
/** 引用 kind 定义(registerRefKind 的注册面)。 */
|
|
38
|
+
interface AiComposerRefDef {
|
|
39
|
+
/** 分组标题(弹层 group label;未知 kind 兜底为 kind 原文) */
|
|
40
|
+
label: string;
|
|
41
|
+
/** 单色 SVG 字符串(经 svgIcon 消毒);缺省用通用文档图标 */
|
|
42
|
+
icon?: string;
|
|
43
|
+
/** 分组排序权重(小者在前);缺省排在已知 kind 之后(最后) */
|
|
44
|
+
order?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 注册(或覆盖)一个引用 kind(第三方扩展入口,照 registerAiKind 的 Map + 兜底模式)。
|
|
48
|
+
* 注册后弹层分组标题/图标/排序取注册表值;未注册的未知 kind 不再静默丢弃——
|
|
49
|
+
* 标题用 kind 原文、图标用通用文档图标、排序排最后。
|
|
50
|
+
*/
|
|
51
|
+
declare function registerRefKind(kind: string, def: AiComposerRefDef): void;
|
|
35
52
|
/**
|
|
36
53
|
* 运行态切换(spec §4.3):
|
|
37
54
|
* el 传 composer 容器或其内部任意元素(含发送钮自身),就近解析 [data-ai-composer]。
|
|
@@ -57,7 +74,9 @@ declare function setComposerModels(el: HTMLElement, providers: AiProviderOption[
|
|
|
57
74
|
*/
|
|
58
75
|
declare function setComposerCommands(el: HTMLElement, commands: AiComposerCommand[]): void;
|
|
59
76
|
/**
|
|
60
|
-
* 配置 @ 引用源(§8):任意位置 token 起点 `@` 触发引用弹层(按 kind
|
|
77
|
+
* 配置 @ 引用源(§8):任意位置 token 起点 `@` 触发引用弹层(按 kind 分组:内置
|
|
78
|
+
* file/folder/doc/agent,可经 registerRefKind 扩展/覆盖;未知 kind 不丢弃——通用文档
|
|
79
|
+
* 图标 + kind 原文兜底,排序缺省排最后)。
|
|
61
80
|
* 选中后文本插入 `@label ` + refs chip +1,派 icen:ai-ref {action:'add', ref};
|
|
62
81
|
* chip × 移除 chip 并删文本里首个 `@label`,派 {action:'remove', ref}。
|
|
63
82
|
*/
|
|
@@ -96,6 +115,12 @@ interface AiComposerBindOpts {
|
|
|
96
115
|
};
|
|
97
116
|
/** 运行态自动管理(默认 true):icen:ai-send → running;icen:ai-done → 解除 */
|
|
98
117
|
running?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* 三弹层(模型/命令/引用)尺寸的整体程序覆盖(PanelSizing 契约):
|
|
120
|
+
* 优先于 composer 根上的 data-panel-*(三弹层共用的用户覆盖面)与各弹层内置默认,
|
|
121
|
+
* 一次设置对三个弹层同时生效。
|
|
122
|
+
*/
|
|
123
|
+
popoverSizing?: PanelSizing;
|
|
99
124
|
}
|
|
100
125
|
interface AiComposerBinding {
|
|
101
126
|
/** 解绑全部监听并中止进行中的托管会话(composer 本身的 init 不受影响) */
|
|
@@ -109,4 +134,4 @@ interface AiComposerBinding {
|
|
|
109
134
|
*/
|
|
110
135
|
declare function bindComposer(el: HTMLElement, opts?: AiComposerBindOpts): AiComposerBinding;
|
|
111
136
|
|
|
112
|
-
export { type AiComposerBindOpts, type AiComposerBinding, type AiComposerCommand, type AiComposerCurrentModel, type AiComposerRefKind, type AiComposerRefSource, type AiProviderModelOption, type AiProviderOption, bindComposer, initAiComposer, setComposerCommands, setComposerModels, setComposerRefs, setComposerRunning, setComposerTodo, setComposerUsage };
|
|
137
|
+
export { type AiComposerBindOpts, type AiComposerBinding, type AiComposerCommand, type AiComposerCurrentModel, type AiComposerRefDef, type AiComposerRefKind, type AiComposerRefSource, type AiProviderModelOption, type AiProviderOption, bindComposer, initAiComposer, registerRefKind, setComposerCommands, setComposerModels, setComposerRefs, setComposerRunning, setComposerTodo, setComposerUsage };
|