sparkdesign 0.4.6 → 0.4.7

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.
Files changed (252) hide show
  1. package/AI_README.md +60 -0
  2. package/README.md +1 -1
  3. package/cli/dist/commands/add.js +1 -1
  4. package/cli/dist/commands/init.js +1 -1
  5. package/cli/registry/AGENTS.md +9 -2
  6. package/cli/registry/agent-manifest.json +794 -0
  7. package/cli/registry/basic/alert.tsx +76 -0
  8. package/cli/registry/basic/aspect-ratio.tsx +8 -0
  9. package/cli/registry/basic/breadcrumb.tsx +117 -0
  10. package/cli/registry/basic/button-group.tsx +79 -0
  11. package/cli/registry/basic/button.tsx +1 -1
  12. package/cli/registry/basic/calendar.tsx +221 -0
  13. package/cli/registry/basic/card.tsx +103 -0
  14. package/cli/registry/basic/carousel.tsx +241 -0
  15. package/cli/registry/basic/chart.tsx +372 -0
  16. package/cli/registry/basic/checkbox.tsx +42 -0
  17. package/cli/registry/basic/collapsible-card.tsx +2 -2
  18. package/cli/registry/basic/combobox.tsx +75 -0
  19. package/cli/registry/basic/command.tsx +184 -0
  20. package/cli/registry/basic/context-menu.tsx +239 -0
  21. package/cli/registry/basic/data-table.tsx +73 -0
  22. package/cli/registry/basic/date-picker.tsx +13 -0
  23. package/cli/registry/basic/dialog.tsx +169 -0
  24. package/cli/registry/basic/direction.tsx +25 -0
  25. package/cli/registry/basic/drawer.tsx +164 -0
  26. package/cli/registry/basic/dropdown-menu.tsx +0 -4
  27. package/cli/registry/basic/empty.tsx +104 -0
  28. package/cli/registry/basic/field.tsx +248 -0
  29. package/cli/registry/basic/hover-card.tsx +58 -0
  30. package/cli/registry/basic/input-group.tsx +168 -0
  31. package/cli/registry/basic/input-otp.tsx +75 -0
  32. package/cli/registry/basic/input.tsx +27 -0
  33. package/cli/registry/basic/item.tsx +204 -0
  34. package/cli/registry/basic/label.tsx +24 -0
  35. package/cli/registry/basic/menubar.tsx +274 -0
  36. package/cli/registry/basic/native-select.tsx +62 -0
  37. package/cli/registry/basic/navigation-menu.tsx +168 -0
  38. package/cli/registry/basic/popover.tsx +59 -0
  39. package/cli/registry/basic/scroll-area.tsx +58 -0
  40. package/cli/registry/basic/select.tsx +2 -1
  41. package/cli/registry/basic/separator.tsx +26 -0
  42. package/cli/registry/basic/sheet.tsx +18 -0
  43. package/cli/registry/basic/textarea.tsx +25 -0
  44. package/cli/registry/basic/toggle.tsx +1 -1
  45. package/cli/registry/basic/typography.tsx +1 -1
  46. package/cli/registry/chat/chat-input/chat-input-textarea.tsx +1 -1
  47. package/cli/registry/chat/chat-input/compound.tsx +4 -3
  48. package/cli/registry/chat/chat-input/context.tsx +4 -1
  49. package/cli/registry/chat/code-block-part.tsx +1 -1
  50. package/cli/registry/chat/conversation-anchor-nav.tsx +349 -0
  51. package/cli/registry/chat/file-attachment.tsx +2 -1
  52. package/cli/registry/chat/file-review-part.tsx +21 -21
  53. package/cli/registry/chat/markdown.tsx +2 -2
  54. package/cli/registry/chat/queue-indicator.tsx +1 -0
  55. package/cli/registry/chat/streaming-markdown-block.tsx +12 -8
  56. package/cli/registry/chat/tool-invocation-card.tsx +4 -1
  57. package/cli/registry/lib/file-icon-maps.ts +22 -22
  58. package/cli/registry/meta.json +518 -0
  59. package/cli/registry/tokens/ontology.json +404 -0
  60. package/cli/registry/tokens/scale/presets/compact.css +16 -5
  61. package/cli/registry/tokens/scale/presets/dense.css +13 -2
  62. package/cli/registry/tokens/scale/presets/sharp.css +18 -6
  63. package/cli/registry/tokens/scale/presets/soft.css +23 -1
  64. package/dist/registry/basic/alert.d.ts +24 -0
  65. package/dist/registry/basic/aspect-ratio.d.ts +16 -0
  66. package/dist/registry/basic/breadcrumb.d.ts +24 -0
  67. package/dist/registry/basic/button-group.d.ts +26 -0
  68. package/dist/registry/basic/button.d.ts +1 -1
  69. package/dist/registry/basic/calendar.d.ts +22 -0
  70. package/dist/registry/basic/card.d.ts +27 -0
  71. package/dist/registry/basic/carousel.d.ts +19 -0
  72. package/dist/registry/basic/chart.d.ts +55 -0
  73. package/dist/registry/basic/checkbox.d.ts +21 -0
  74. package/dist/registry/basic/combobox.d.ts +26 -0
  75. package/dist/registry/basic/command.d.ts +18 -0
  76. package/dist/registry/basic/context-menu.d.ts +44 -0
  77. package/dist/registry/basic/data-table.d.ts +26 -0
  78. package/dist/registry/basic/date-picker.d.ts +18 -0
  79. package/dist/registry/basic/dialog.d.ts +39 -0
  80. package/dist/registry/basic/direction.d.ts +19 -0
  81. package/dist/registry/basic/drawer.d.ts +37 -0
  82. package/dist/registry/basic/empty.d.ts +22 -0
  83. package/dist/registry/basic/field.d.ts +24 -0
  84. package/dist/registry/basic/hover-card.d.ts +22 -0
  85. package/dist/registry/basic/input-group.d.ts +27 -0
  86. package/dist/registry/basic/input-otp.d.ts +22 -0
  87. package/dist/registry/basic/input.d.ts +15 -0
  88. package/dist/registry/basic/item.d.ts +34 -0
  89. package/dist/registry/basic/label.d.ts +16 -0
  90. package/dist/registry/basic/menubar.d.ts +37 -0
  91. package/dist/registry/basic/native-select.d.ts +18 -0
  92. package/dist/registry/basic/navigation-menu.d.ts +25 -0
  93. package/dist/registry/basic/popover.d.ts +23 -0
  94. package/dist/registry/basic/scroll-area.d.ts +5 -0
  95. package/dist/registry/basic/separator.d.ts +16 -0
  96. package/dist/registry/basic/sheet.d.ts +13 -0
  97. package/dist/registry/basic/textarea.d.ts +15 -0
  98. package/dist/registry/basic/toggle.d.ts +1 -1
  99. package/dist/registry/chat/chat-input/context.d.ts +3 -1
  100. package/dist/registry/chat/conversation-anchor-nav.d.ts +72 -0
  101. package/dist/registry/chat/tool-invocation-card.d.ts +2 -0
  102. package/dist/scale/presets/compact.css +16 -5
  103. package/dist/scale/presets/dense.css +13 -2
  104. package/dist/scale/presets/sharp.css +18 -6
  105. package/dist/scale/presets/soft.css +23 -1
  106. package/dist/spark-design.cjs.js +40 -36
  107. package/dist/spark-design.es.js +8647 -8657
  108. package/dist/sparkdesign.css +1 -1
  109. package/dist/src/components/basic/Alert/index.d.ts +13 -0
  110. package/dist/src/components/basic/AspectRatio/index.d.ts +13 -0
  111. package/dist/src/components/basic/Breadcrumb/index.d.ts +12 -0
  112. package/dist/src/components/basic/ButtonGroup/index.d.ts +13 -0
  113. package/dist/src/components/basic/Calendar/index.d.ts +13 -0
  114. package/dist/src/components/basic/Card/index.d.ts +13 -0
  115. package/dist/src/components/basic/Carousel/index.d.ts +12 -0
  116. package/dist/src/components/basic/Chart/index.d.ts +13 -0
  117. package/dist/src/components/basic/Checkbox/index.d.ts +13 -0
  118. package/dist/src/components/basic/Combobox/index.d.ts +13 -0
  119. package/dist/src/components/basic/Command/index.d.ts +12 -0
  120. package/dist/src/components/basic/ContextMenu/index.d.ts +19 -0
  121. package/dist/src/components/basic/DataTable/index.d.ts +13 -0
  122. package/dist/src/components/basic/DatePicker/index.d.ts +13 -0
  123. package/dist/src/components/basic/Dialog/index.d.ts +16 -0
  124. package/dist/src/components/basic/Direction/index.d.ts +13 -0
  125. package/dist/src/components/basic/Drawer/index.d.ts +16 -0
  126. package/dist/src/components/basic/Empty/index.d.ts +12 -0
  127. package/dist/src/components/basic/Field/index.d.ts +12 -0
  128. package/dist/src/components/basic/HoverCard/index.d.ts +16 -0
  129. package/dist/src/components/basic/Input/index.d.ts +13 -0
  130. package/dist/src/components/basic/InputGroup/index.d.ts +12 -0
  131. package/dist/src/components/basic/InputOTP/index.d.ts +12 -0
  132. package/dist/src/components/basic/Item/index.d.ts +12 -0
  133. package/dist/src/components/basic/Label/index.d.ts +13 -0
  134. package/dist/src/components/basic/Menubar/index.d.ts +12 -0
  135. package/dist/src/components/basic/NativeSelect/index.d.ts +12 -0
  136. package/dist/src/components/basic/NavigationMenu/index.d.ts +12 -0
  137. package/dist/src/components/basic/Popover/index.d.ts +16 -0
  138. package/dist/src/components/basic/ScrollArea/index.d.ts +12 -0
  139. package/dist/src/components/basic/Separator/index.d.ts +13 -0
  140. package/dist/src/components/basic/Sheet/index.d.ts +13 -0
  141. package/dist/src/components/basic/Textarea/index.d.ts +13 -0
  142. package/dist/src/components/chat/ConversationAnchorNav/index.d.ts +13 -0
  143. package/dist/src/components/chat/StreamingMarkdownBlock/index.d.ts +13 -0
  144. package/dist/src/components/index.d.ts +57 -0
  145. package/dist/tokens/AGENTS.md +1 -0
  146. package/dist/tokens/scale/presets/compact.css +16 -5
  147. package/dist/tokens/scale/presets/dense.css +13 -2
  148. package/dist/tokens/scale/presets/sharp.css +18 -6
  149. package/dist/tokens/scale/presets/soft.css +23 -1
  150. package/docs/agent/component-selection.md +60 -0
  151. package/docs/agent/token-ontology.md +37 -0
  152. package/package.json +31 -5
  153. package/registry/agent-manifest.json +794 -0
  154. package/registry/tokens/ontology.json +404 -0
  155. package/dist/_basePickBy-DnQN8w3y.js +0 -151
  156. package/dist/_basePickBy-a-kPMlkg.cjs +0 -1
  157. package/dist/_baseUniq-B-N2NQ50.js +0 -614
  158. package/dist/_baseUniq-Cc_zbSif.cjs +0 -1
  159. package/dist/arc-BQBhijZ6.js +0 -83
  160. package/dist/arc-mWQt0Yph.cjs +0 -1
  161. package/dist/architectureDiagram-VXUJARFQ-BMZEucno.cjs +0 -36
  162. package/dist/architectureDiagram-VXUJARFQ-DTdjD3Bp.js +0 -4661
  163. package/dist/blockDiagram-VD42YOAC-CzHn0yob.js +0 -2256
  164. package/dist/blockDiagram-VD42YOAC-DDxdHAlz.cjs +0 -122
  165. package/dist/c4Diagram-YG6GDRKO-4Gz0I4gj.cjs +0 -10
  166. package/dist/c4Diagram-YG6GDRKO-BIy--yVN.js +0 -1580
  167. package/dist/channel-BQn0o8bs.js +0 -5
  168. package/dist/channel-DaN7XniJ.cjs +0 -1
  169. package/dist/chunk-4BX2VUAB-BlQFTQqz.cjs +0 -1
  170. package/dist/chunk-4BX2VUAB-Czitj3Kc.js +0 -8
  171. package/dist/chunk-55IACEB6-DXacNZbO.js +0 -8
  172. package/dist/chunk-55IACEB6-DnDxpye9.cjs +0 -1
  173. package/dist/chunk-B4BG7PRW-CBdN0q_V.js +0 -1375
  174. package/dist/chunk-B4BG7PRW-DbGvUkGO.cjs +0 -165
  175. package/dist/chunk-DI55MBZ5-D1YJMs6x.cjs +0 -220
  176. package/dist/chunk-DI55MBZ5-NCQTvayw.js +0 -1370
  177. package/dist/chunk-FMBD7UC4-CsGMbrtr.js +0 -19
  178. package/dist/chunk-FMBD7UC4-Di7cUUh5.cjs +0 -15
  179. package/dist/chunk-QN33PNHL-0j5LC8Lm.cjs +0 -1
  180. package/dist/chunk-QN33PNHL-3GERZBRm.js +0 -19
  181. package/dist/chunk-QZHKN3VN-AVEY9ImQ.js +0 -15
  182. package/dist/chunk-QZHKN3VN-s8Z0a8mc.cjs +0 -1
  183. package/dist/chunk-TZMSLE5B-CAf87HPt.cjs +0 -1
  184. package/dist/chunk-TZMSLE5B-sbiflal0.js +0 -64
  185. package/dist/classDiagram-2ON5EDUG-Ct9JLIN2.cjs +0 -1
  186. package/dist/classDiagram-2ON5EDUG-Dzfrft3a.js +0 -16
  187. package/dist/classDiagram-v2-WZHVMYZB-Ct9JLIN2.cjs +0 -1
  188. package/dist/classDiagram-v2-WZHVMYZB-Dzfrft3a.js +0 -16
  189. package/dist/clone-Cde_NQ8V.js +0 -8
  190. package/dist/clone-DCNjWuM2.cjs +0 -1
  191. package/dist/cose-bilkent-S5V4N54A-0uLijMro.cjs +0 -1
  192. package/dist/cose-bilkent-S5V4N54A-Bb08N431.js +0 -2608
  193. package/dist/cytoscape.esm-CNUX3VTg.cjs +0 -321
  194. package/dist/cytoscape.esm-Cvf3sx9F.js +0 -18704
  195. package/dist/dagre-6UL2VRFP-CY_Wz5Zd.js +0 -444
  196. package/dist/dagre-6UL2VRFP-Dxe7_qZc.cjs +0 -4
  197. package/dist/defaultLocale-BgPVtth8.js +0 -171
  198. package/dist/defaultLocale-C4wbwF1n.cjs +0 -1
  199. package/dist/diagram-PSM6KHXK-D2bdb7MT.js +0 -531
  200. package/dist/diagram-PSM6KHXK-YF69SUjY.cjs +0 -24
  201. package/dist/diagram-QEK2KX5R-BpUSoh0-.js +0 -217
  202. package/dist/diagram-QEK2KX5R-DZPGteon.cjs +0 -43
  203. package/dist/diagram-S2PKOQOG-ht-zdvFG.cjs +0 -24
  204. package/dist/diagram-S2PKOQOG-zFeLJ50Z.js +0 -142
  205. package/dist/erDiagram-Q2GNP2WA-B38iJ6ts.js +0 -841
  206. package/dist/erDiagram-Q2GNP2WA-RgS80DDU.cjs +0 -60
  207. package/dist/flowDiagram-NV44I4VS-BHilOs2p.cjs +0 -162
  208. package/dist/flowDiagram-NV44I4VS-BrBJcoce.js +0 -1620
  209. package/dist/ganttDiagram-JELNMOA3-pZiJeFio.cjs +0 -267
  210. package/dist/ganttDiagram-JELNMOA3-tw6FhkWJ.js +0 -2670
  211. package/dist/gitGraphDiagram-V2S2FVAM-BWn5uIK5.js +0 -699
  212. package/dist/gitGraphDiagram-V2S2FVAM-DKKeG-9R.cjs +0 -65
  213. package/dist/graph-DIbblrZP.cjs +0 -1
  214. package/dist/graph-DPcK91G3.js +0 -247
  215. package/dist/infoDiagram-HS3SLOUP-B8gwwhct.cjs +0 -2
  216. package/dist/infoDiagram-HS3SLOUP-D47PNcP_.js +0 -24
  217. package/dist/init-CHZsXQcr.cjs +0 -1
  218. package/dist/init-DjUOC4st.js +0 -16
  219. package/dist/journeyDiagram-XKPGCS4Q-BG3cfhyU.js +0 -834
  220. package/dist/journeyDiagram-XKPGCS4Q-D8DVLJof.cjs +0 -139
  221. package/dist/kanban-definition-3W4ZIXB7-4OCnEouP.cjs +0 -89
  222. package/dist/kanban-definition-3W4ZIXB7-CWi_ssF9.js +0 -719
  223. package/dist/layout-Byuh8f-J.cjs +0 -1
  224. package/dist/layout-CdLdvj1j.js +0 -1335
  225. package/dist/linear-C2Q_PI9B.js +0 -259
  226. package/dist/linear-C69aPBW1.cjs +0 -1
  227. package/dist/mermaid.core-DBwAx_jp.cjs +0 -249
  228. package/dist/mermaid.core-gFR0XUlD.js +0 -15300
  229. package/dist/mindmap-definition-VGOIOE7T-8P7obVV4.cjs +0 -68
  230. package/dist/mindmap-definition-VGOIOE7T-DnOa7WJ9.js +0 -784
  231. package/dist/ordinal-B6-f3MAq.js +0 -61
  232. package/dist/ordinal-CagbB1m8.cjs +0 -1
  233. package/dist/pieDiagram-ADFJNKIX-5NAlvhMo.js +0 -161
  234. package/dist/pieDiagram-ADFJNKIX-CQBG4yR9.cjs +0 -30
  235. package/dist/quadrantDiagram-AYHSOK5B-Oe4y7RZ0.cjs +0 -7
  236. package/dist/quadrantDiagram-AYHSOK5B-rh2DPEP1.js +0 -1022
  237. package/dist/requirementDiagram-UZGBJVZJ-DcWaCuXr.js +0 -850
  238. package/dist/requirementDiagram-UZGBJVZJ-gfdlrFiq.cjs +0 -64
  239. package/dist/sankeyDiagram-TZEHDZUN-CQIKFwD0.js +0 -810
  240. package/dist/sankeyDiagram-TZEHDZUN-DvPtzQvC.cjs +0 -10
  241. package/dist/sequenceDiagram-WL72ISMW-BNrsMagL.cjs +0 -145
  242. package/dist/sequenceDiagram-WL72ISMW-iCX3ckKx.js +0 -2511
  243. package/dist/stateDiagram-FKZM4ZOC-DBvJ_eeL.cjs +0 -1
  244. package/dist/stateDiagram-FKZM4ZOC-ZVsJlaHJ.js +0 -263
  245. package/dist/stateDiagram-v2-4FDKWEC3-CB_nTHcE.js +0 -16
  246. package/dist/stateDiagram-v2-4FDKWEC3-Xkx17v6T.cjs +0 -1
  247. package/dist/timeline-definition-IT6M3QCI-BmGkYQiz.cjs +0 -61
  248. package/dist/timeline-definition-IT6M3QCI-Ck8zTt6w.js +0 -795
  249. package/dist/treemap-GDKQZRPO-B9sfERx8.js +0 -17922
  250. package/dist/treemap-GDKQZRPO-BVfJRs0Z.cjs +0 -160
  251. package/dist/xychartDiagram-PRI3JC2R-By_S8NzN.js +0 -1340
  252. package/dist/xychartDiagram-PRI3JC2R-CNfDrGxM.cjs +0 -7
@@ -0,0 +1,349 @@
1
+ 'use client'
2
+
3
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
4
+ import type { RefObject, ReactNode } from 'react'
5
+ import { AnimatePresence, motion } from 'framer-motion'
6
+ import { cn } from '@/lib/utils'
7
+ import { Tooltip } from '../basic/tooltip'
8
+
9
+ // ============================================================================
10
+ // 对话锚点导航条(Conversation Anchor Nav)
11
+ // ----------------------------------------------------------------------------
12
+ // 固定在长对话流容器右侧居中,为每条用户消息(或任何可定位节点)渲染一枚短横杠
13
+ // 或「打孔」圆点。hover 时横杠加长 / 圆点放大并展示摘要 tooltip,点击触发 onSelect。
14
+ // 当前可视区域对应的锚点自动高亮,数量不足 minItems 或 hidden=true 时自动隐藏。
15
+ // ============================================================================
16
+
17
+ /** 单个锚点项 */
18
+ export interface ConversationAnchorItem {
19
+ /** 稳定 id,用于定位 DOM 节点与 React key */
20
+ id: string
21
+ /** 在 Tooltip 中展示的摘要文案(纯文本)。为空时回落到 labels.scrollTo。 */
22
+ summary?: string
23
+ }
24
+
25
+ /** i18n 文案 */
26
+ export interface ConversationAnchorLabels {
27
+ /** nav 的 aria-label */
28
+ ariaLabel?: string
29
+ /** Tooltip 兜底文案(无摘要时) */
30
+ scrollTo?: string
31
+ }
32
+
33
+ /** 样式变体 */
34
+ export type ConversationAnchorVariant = 'bar' | 'punch'
35
+
36
+ export interface ConversationAnchorNavProps {
37
+ /** 滚动容器 ref,用于检测当前可视锚点;受控场景可传 { current: null } */
38
+ scrollContainerRef: RefObject<HTMLElement | null>
39
+ /** 锚点列表,顺序即视觉顺序 */
40
+ items: ConversationAnchorItem[]
41
+ /** 点击锚点时回调,参数为在 items 中的索引与对应 item */
42
+ onSelect: (index: number, item: ConversationAnchorItem) => void
43
+ /**
44
+ * 当前高亮索引(受控)。
45
+ * 省略时组件内部会订阅 scrollContainerRef 的滚动并通过 IntersectionObserver 自动推导。
46
+ */
47
+ activeIndex?: number
48
+ /**
49
+ * 锚点视觉样式:
50
+ * - 'bar' (默认):短横杠,hover 时加长
51
+ * - 'punch':圆形"打孔"点,适合羊皮纸 / 拟物风主题
52
+ */
53
+ variant?: ConversationAnchorVariant
54
+ /** 少于该数量时隐藏整个 nav,默认 3 */
55
+ minItems?: number
56
+ /** 外部强制隐藏(例如 compact 布局),默认 false */
57
+ hidden?: boolean
58
+ /** 自定义 Tooltip / aria 文案 */
59
+ labels?: ConversationAnchorLabels
60
+ /** 额外 className(会与默认定位 className 合并) */
61
+ className?: string
62
+ /**
63
+ * 自定义锚点元素解析。默认通过
64
+ * `scrollContainerRef.current.querySelector('[data-spark-anchor="<id>"]')`
65
+ * 查找;需要业务在消息容器上添加该属性。
66
+ */
67
+ getAnchorElement?: (
68
+ item: ConversationAnchorItem,
69
+ index: number,
70
+ container: HTMLElement,
71
+ ) => HTMLElement | null
72
+ /** 渲染 slot:自定义每个锚点的视觉表达,返回 null 时回落到内置样式 */
73
+ renderAnchor?: (ctx: {
74
+ item: ConversationAnchorItem
75
+ index: number
76
+ isActive: boolean
77
+ variant: ConversationAnchorVariant
78
+ }) => ReactNode
79
+ }
80
+
81
+ const DEFAULT_LABELS: Required<ConversationAnchorLabels> = {
82
+ ariaLabel: 'Conversation anchors',
83
+ scrollTo: 'Scroll to message',
84
+ }
85
+
86
+ export const ConversationAnchorNav = memo(function ConversationAnchorNav({
87
+ scrollContainerRef,
88
+ items,
89
+ onSelect,
90
+ activeIndex: controlledActiveIndex,
91
+ variant = 'bar',
92
+ minItems = 3,
93
+ hidden = false,
94
+ labels,
95
+ className,
96
+ getAnchorElement,
97
+ renderAnchor,
98
+ }: ConversationAnchorNavProps) {
99
+ const mergedLabels = { ...DEFAULT_LABELS, ...labels }
100
+
101
+ const detectedActiveIndex = useVisibleAnchorIndex({
102
+ scrollContainerRef,
103
+ items,
104
+ getAnchorElement,
105
+ enabled: controlledActiveIndex === undefined && !hidden,
106
+ })
107
+
108
+ const activeIndex =
109
+ controlledActiveIndex !== undefined ? controlledActiveIndex : detectedActiveIndex
110
+
111
+ const handleClick = useCallback(
112
+ (index: number) => {
113
+ const item = items[index]
114
+ if (item) onSelect(index, item)
115
+ },
116
+ [items, onSelect],
117
+ )
118
+
119
+ const shouldShow = !hidden && items.length >= minItems
120
+
121
+ return (
122
+ <AnimatePresence mode="wait">
123
+ {shouldShow && (
124
+ <motion.nav
125
+ key="spark-conversation-anchor-nav"
126
+ initial={{ opacity: 0, x: 8 }}
127
+ animate={{
128
+ opacity: 1,
129
+ x: 0,
130
+ transition: { type: 'spring', stiffness: 500, damping: 30, mass: 0.6 },
131
+ }}
132
+ exit={{
133
+ opacity: 0,
134
+ x: 8,
135
+ transition: { duration: 0.15, ease: [0.32, 0.72, 0, 1] },
136
+ }}
137
+ className={cn(
138
+ 'absolute right-3 top-1/2 -translate-y-1/2 z-[40]',
139
+ 'flex flex-col items-end gap-1.5 max-h-[80%] overflow-y-auto',
140
+ 'scrollbar-hide',
141
+ className,
142
+ )}
143
+ aria-label={mergedLabels.ariaLabel}
144
+ >
145
+ {items.map((item, index) => (
146
+ <AnchorDot
147
+ key={item.id}
148
+ item={item}
149
+ index={index}
150
+ isActive={index === activeIndex}
151
+ variant={variant}
152
+ tooltipFallback={mergedLabels.scrollTo}
153
+ onClick={handleClick}
154
+ renderAnchor={renderAnchor}
155
+ />
156
+ ))}
157
+ </motion.nav>
158
+ )}
159
+ </AnimatePresence>
160
+ )
161
+ })
162
+
163
+ ConversationAnchorNav.displayName = 'ConversationAnchorNav'
164
+
165
+ // ----------------------------------------------------------------------------
166
+ // 单个锚点
167
+ // ----------------------------------------------------------------------------
168
+
169
+ interface AnchorDotProps {
170
+ item: ConversationAnchorItem
171
+ index: number
172
+ isActive: boolean
173
+ variant: ConversationAnchorVariant
174
+ tooltipFallback: string
175
+ onClick: (index: number) => void
176
+ renderAnchor?: ConversationAnchorNavProps['renderAnchor']
177
+ }
178
+
179
+ const AnchorDot = memo(
180
+ function AnchorDot({
181
+ item,
182
+ index,
183
+ isActive,
184
+ variant,
185
+ tooltipFallback,
186
+ onClick,
187
+ renderAnchor,
188
+ }: AnchorDotProps) {
189
+ const handleClick = useCallback(() => onClick(index), [onClick, index])
190
+
191
+ const custom = renderAnchor
192
+ ? renderAnchor({ item, index, isActive, variant })
193
+ : null
194
+
195
+ return (
196
+ <Tooltip
197
+ content={item.summary || tooltipFallback}
198
+ side="left"
199
+ contentClassName="max-w-xs"
200
+ >
201
+ <button
202
+ type="button"
203
+ onClick={handleClick}
204
+ className={cn(
205
+ // 使用命名 group(group/spark-anchor)避免被外层 `.group` 祖先的 hover 污染
206
+ 'group/spark-anchor flex cursor-pointer border-0 bg-transparent outline-none',
207
+ 'focus-visible:ring-2 focus-visible:ring-[var(--color-link)] focus-visible:rounded-full',
208
+ variant === 'punch'
209
+ ? 'h-7 w-7 shrink-0 items-center justify-center p-0'
210
+ : 'items-center justify-end p-1',
211
+ )}
212
+ aria-label={`${tooltipFallback} ${index + 1}`}
213
+ aria-current={isActive ? 'true' : undefined}
214
+ data-spark-anchor-nav-item={item.id}
215
+ >
216
+ {custom ?? (
217
+ <span
218
+ className={cn(
219
+ 'block shrink-0 origin-center',
220
+ variant === 'punch'
221
+ ? cn(
222
+ 'rounded-full transition-transform duration-300 ease-out',
223
+ isActive
224
+ ? 'h-2 w-2 bg-[var(--color-link)] group-hover/spark-anchor:scale-125'
225
+ : cn(
226
+ 'h-1.5 w-1.5 border border-solid border-border-tertiary bg-transparent',
227
+ 'group-hover/spark-anchor:scale-150 group-hover/spark-anchor:border-[var(--color-link)]',
228
+ ),
229
+ )
230
+ : cn(
231
+ 'rounded-full transition-all duration-300 ease-out h-0.5',
232
+ isActive
233
+ ? 'w-4 bg-text-quaternary'
234
+ : 'w-2.5 bg-border-tertiary',
235
+ 'group-hover/spark-anchor:w-6 group-hover/spark-anchor:bg-text-quaternary',
236
+ ),
237
+ )}
238
+ />
239
+ )}
240
+ </button>
241
+ </Tooltip>
242
+ )
243
+ },
244
+ (prev, next) =>
245
+ prev.isActive === next.isActive &&
246
+ prev.variant === next.variant &&
247
+ prev.index === next.index &&
248
+ prev.item.id === next.item.id &&
249
+ prev.item.summary === next.item.summary &&
250
+ prev.tooltipFallback === next.tooltipFallback &&
251
+ prev.onClick === next.onClick &&
252
+ prev.renderAnchor === next.renderAnchor,
253
+ )
254
+
255
+ // ----------------------------------------------------------------------------
256
+ // Hook: 基于 IntersectionObserver 推导当前可视锚点索引
257
+ // ----------------------------------------------------------------------------
258
+
259
+ export interface UseVisibleAnchorIndexOptions {
260
+ scrollContainerRef: RefObject<HTMLElement | null>
261
+ items: ConversationAnchorItem[]
262
+ /** 与 ConversationAnchorNav 同名 prop,默认按 `[data-spark-anchor="<id>"]` 查找 */
263
+ getAnchorElement?: ConversationAnchorNavProps['getAnchorElement']
264
+ /** 为 false 时不订阅滚动,返回 0;用于受控场景 */
265
+ enabled?: boolean
266
+ }
267
+
268
+ /**
269
+ * 订阅 scroll 容器内每个锚点元素的可见性,返回"最靠近顶部且仍可见"的锚点索引。
270
+ * - 容器必须可滚动(通常 overflow-y: auto),且锚点元素是其后代
271
+ * - 每个锚点需要能通过 `getAnchorElement` 解析到 HTMLElement
272
+ */
273
+ export function useVisibleAnchorIndex({
274
+ scrollContainerRef,
275
+ items,
276
+ getAnchorElement,
277
+ enabled = true,
278
+ }: UseVisibleAnchorIndexOptions): number {
279
+ const [activeIndex, setActiveIndex] = useState(0)
280
+
281
+ // 稳定化 ids 作为 effect 依赖,避免每次 items 引用变化都重建 observer
282
+ const idsKey = useMemo(() => items.map((i) => i.id).join('|'), [items])
283
+
284
+ const getAnchorRef = useRef(getAnchorElement)
285
+ useEffect(() => {
286
+ getAnchorRef.current = getAnchorElement
287
+ }, [getAnchorElement])
288
+
289
+ useEffect(() => {
290
+ if (!enabled) return
291
+ const container = scrollContainerRef.current
292
+ if (!container || items.length === 0) return
293
+ if (typeof IntersectionObserver === 'undefined') return
294
+
295
+ const resolve = (item: ConversationAnchorItem, index: number) => {
296
+ const fn = getAnchorRef.current
297
+ if (fn) return fn(item, index, container)
298
+ return container.querySelector<HTMLElement>(
299
+ `[data-spark-anchor="${cssEscape(item.id)}"]`,
300
+ )
301
+ }
302
+
303
+ const anchors: Array<{ index: number; el: HTMLElement }> = []
304
+ items.forEach((item, index) => {
305
+ const el = resolve(item, index)
306
+ if (el) anchors.push({ index, el })
307
+ })
308
+ if (anchors.length === 0) return
309
+
310
+ const ratioByEl = new WeakMap<HTMLElement, number>()
311
+
312
+ const recompute = () => {
313
+ let bestIndex = anchors[0]?.index ?? 0
314
+ let bestRatio = -1
315
+ for (const { index, el } of anchors) {
316
+ const r = ratioByEl.get(el) ?? 0
317
+ if (r > bestRatio) {
318
+ bestRatio = r
319
+ bestIndex = index
320
+ }
321
+ }
322
+ setActiveIndex((prev) => (prev === bestIndex ? prev : bestIndex))
323
+ }
324
+
325
+ const observer = new IntersectionObserver(
326
+ (entries) => {
327
+ for (const e of entries) {
328
+ ratioByEl.set(e.target as HTMLElement, e.intersectionRatio)
329
+ }
330
+ recompute()
331
+ },
332
+ { root: container, threshold: [0, 0.25, 0.5, 0.75, 1] },
333
+ )
334
+
335
+ anchors.forEach((a) => observer.observe(a.el))
336
+
337
+ return () => observer.disconnect()
338
+ // idsKey 稳定覆盖 items 内容变化;scrollContainerRef 本身稳定
339
+ }, [enabled, scrollContainerRef, items, idsKey])
340
+
341
+ return activeIndex
342
+ }
343
+
344
+ function cssEscape(value: string): string {
345
+ if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
346
+ return CSS.escape(value)
347
+ }
348
+ return value.replace(/["\\]/g, '\\$&')
349
+ }
@@ -20,7 +20,8 @@ export const FileAttachment = forwardRef<HTMLSpanElement, FileAttachmentProps>(
20
20
  const resolvedIcon: ReactNode =
21
21
  icon ?? (getFileIcon ? getFileIcon(lookup, ICON_CLASS) : <FileLine className={ICON_CLASS} />)
22
22
 
23
- const { color: _omitColor, ...restProps } = props
23
+ const { color, ...restProps } = props
24
+ void color
24
25
  return (
25
26
  <Tag
26
27
  ref={ref}
@@ -34,27 +34,27 @@ const SETI_CHAR_MAP: Record<string, string> = {
34
34
  default: '\ue023',
35
35
  }
36
36
  const SETI_COLOR_MAP: Record<string, string> = {
37
- javascript: '#F8CA3E',
38
- typescript: '#1E8DEE',
39
- react: '#1E8DEE',
40
- json: '#F8CA3E',
41
- html: '#1E8DEE',
42
- css: '#1E8DEE',
43
- python: '#1E8DEE',
44
- java: '#E84654',
45
- go: '#1E8DEE',
46
- rust: '#6A89E6',
47
- php: '#AE4EE2',
48
- ruby: '#E84654',
49
- vue: '#4EBD35',
50
- svelte: '#E84654',
51
- markdown: '#1E8DEE',
52
- yaml: '#AE4EE2',
53
- xml: '#F18745',
54
- shell: '#4EBD35',
55
- sass: '#E84654',
56
- settings: '#6A89E6',
57
- default: '#6A89E6',
37
+ javascript: 'var(--color-yellow)',
38
+ typescript: 'var(--color-blue)',
39
+ react: 'var(--color-blue)',
40
+ json: 'var(--color-yellow)',
41
+ html: 'var(--color-blue)',
42
+ css: 'var(--color-blue)',
43
+ python: 'var(--color-blue)',
44
+ java: 'var(--color-error)',
45
+ go: 'var(--color-blue)',
46
+ rust: 'var(--color-lavender)',
47
+ php: 'var(--color-purple)',
48
+ ruby: 'var(--color-error)',
49
+ vue: 'var(--color-success)',
50
+ svelte: 'var(--color-error)',
51
+ markdown: 'var(--color-blue)',
52
+ yaml: 'var(--color-purple)',
53
+ xml: 'var(--color-orange)',
54
+ shell: 'var(--color-success)',
55
+ sass: 'var(--color-error)',
56
+ settings: 'var(--color-lavender)',
57
+ default: 'var(--color-lavender)',
58
58
  }
59
59
  const FILE_TYPE_MAP: Record<string, string> = {
60
60
  js: 'javascript',
@@ -87,7 +87,7 @@ const markdownComponents: Components = {
87
87
  if (!isBlock) {
88
88
  return (
89
89
  <code
90
- className="relative rounded bg-fill-tertiary px-[0.3rem] py-[0.2rem] font-mono text-sm leading-sm font-semibold text-text"
90
+ className="relative rounded bg-fill-tertiary px-1 py-0.5 font-mono text-sm leading-sm font-semibold text-text"
91
91
  {...props}
92
92
  >
93
93
  {children}
@@ -134,7 +134,7 @@ export function MarkdownBody({ children, className }: MarkdownBodyProps) {
134
134
  <div className={cn('markdown-body text-sm leading-sm text-text', className)}>
135
135
  <ReactMarkdown
136
136
  remarkPlugins={[remarkGfm, remarkMath]}
137
- rehypePlugins={[rehypeKatex as any]}
137
+ rehypePlugins={[rehypeKatex]}
138
138
  components={markdownComponents}
139
139
  >
140
140
  {children}
@@ -132,6 +132,7 @@ export const QueueIndicator = memo(function QueueIndicator({
132
132
  closeIcon,
133
133
  arrowDownSIcon,
134
134
  }: QueueIndicatorProps) {
135
+ void isStreaming
135
136
  const labels = { ...DEFAULT_LABELS, ...labelsProp }
136
137
 
137
138
  const defaultArrowUp = arrowUpIcon ?? <ArrowUpLine className={arrowUpClass} />
@@ -52,19 +52,23 @@ export function StreamingMarkdownBlock({
52
52
  className,
53
53
  }: StreamingMarkdownBlockProps) {
54
54
  const tokens = useMemo(() => segmentForStreaming(content), [content])
55
- const [visibleCount, setVisibleCount] = useState(0)
55
+ const [streamState, setStreamState] = useState({ content, visibleCount: isActive ? 0 : tokens.length })
56
56
  const onStreamCompleteRef = useRef(onStreamComplete)
57
- onStreamCompleteRef.current = onStreamComplete
58
57
 
59
- const visibleText = useMemo(
60
- () => tokens.slice(0, visibleCount).join(''),
61
- [tokens, visibleCount]
62
- )
58
+ useEffect(() => {
59
+ onStreamCompleteRef.current = onStreamComplete
60
+ }, [onStreamComplete])
61
+
62
+ const visibleCount = streamState.content === content
63
+ ? streamState.visibleCount
64
+ : isActive
65
+ ? 0
66
+ : tokens.length
67
+ const visibleText = useMemo(() => tokens.slice(0, visibleCount).join(''), [tokens, visibleCount])
63
68
  const { completed, partial } = useMemo(() => splitCompletedBlocks(visibleText), [visibleText])
64
69
 
65
70
  useEffect(() => {
66
71
  if (!isActive) return
67
- setVisibleCount(0)
68
72
  const len = tokens.length
69
73
  if (len === 0) {
70
74
  onStreamCompleteRef.current?.()
@@ -73,7 +77,7 @@ export function StreamingMarkdownBlock({
73
77
  let k = 0
74
78
  const id = setInterval(() => {
75
79
  k = Math.min(k + TOKENS_PER_TICK, len)
76
- setVisibleCount(k)
80
+ setStreamState({ content, visibleCount: k })
77
81
  if (k >= len) {
78
82
  clearInterval(id)
79
83
  onStreamCompleteRef.current?.()
@@ -18,6 +18,8 @@ export interface ToolInvocationCardProps {
18
18
  checkboxCircleIcon?: ReactNode
19
19
  closeCircleIcon?: ReactNode
20
20
  arrowDownIcon?: ReactNode
21
+ /** @deprecated Use arrowDownIcon. */
22
+ arrowRightIcon?: ReactNode
21
23
  runningText?: string
22
24
  ranText?: string
23
25
  failedText?: string
@@ -43,6 +45,7 @@ export const ToolInvocationCard = memo(function ToolInvocationCard({
43
45
  checkboxCircleIcon,
44
46
  closeCircleIcon,
45
47
  arrowDownIcon,
48
+ arrowRightIcon,
46
49
  runningText = 'Running',
47
50
  ranText = 'Ran',
48
51
  failedText = 'Failed',
@@ -62,7 +65,7 @@ export const ToolInvocationCard = memo(function ToolInvocationCard({
62
65
  const defaultTime = timeIcon ?? <TimeLine className={timeClass} />
63
66
  const defaultCheck = checkboxCircleIcon ?? <CheckboxCircleLine className={checkboxClass} />
64
67
  const defaultCloseCircle = closeCircleIcon ?? <CloseCircleLine className={closeCircleClass} />
65
- const defaultArrow = arrowDownIcon ?? <ArrowDownSLine className={arrowClass} />
68
+ const defaultArrow = arrowDownIcon ?? arrowRightIcon ?? <ArrowDownSLine className={arrowClass} />
66
69
 
67
70
  return (
68
71
  <div className={cn('flex flex-col', className)}>
@@ -27,30 +27,30 @@ export const SETI_CHAR_MAP: Record<string, string> = {
27
27
  }
28
28
 
29
29
  // ═══════════════════════════════════════════════════════════════════════════════
30
- // Seti 图标颜色(hex,用于 Seti 字体渲染 style.color)
30
+ // Seti 图标颜色(tokenized,用于 Seti 字体渲染 style.color)
31
31
  // ═══════════════════════════════════════════════════════════════════════════════
32
32
  export const SETI_COLOR_MAP: Record<string, string> = {
33
- javascript: '#F8CA3E',
34
- typescript: '#1E8DEE',
35
- react: '#1E8DEE',
36
- json: '#F8CA3E',
37
- html: '#1E8DEE',
38
- css: '#1E8DEE',
39
- python: '#1E8DEE',
40
- java: '#E84654',
41
- go: '#1E8DEE',
42
- rust: '#6A89E6',
43
- php: '#AE4EE2',
44
- ruby: '#E84654',
45
- vue: '#4EBD35',
46
- svelte: '#E84654',
47
- markdown: '#1E8DEE',
48
- yaml: '#AE4EE2',
49
- xml: '#F18745',
50
- shell: '#4EBD35',
51
- sass: '#E84654',
52
- settings: '#6A89E6',
53
- default: '#6A89E6',
33
+ javascript: 'var(--color-yellow)',
34
+ typescript: 'var(--color-blue)',
35
+ react: 'var(--color-blue)',
36
+ json: 'var(--color-yellow)',
37
+ html: 'var(--color-blue)',
38
+ css: 'var(--color-blue)',
39
+ python: 'var(--color-blue)',
40
+ java: 'var(--color-error)',
41
+ go: 'var(--color-blue)',
42
+ rust: 'var(--color-lavender)',
43
+ php: 'var(--color-purple)',
44
+ ruby: 'var(--color-error)',
45
+ vue: 'var(--color-success)',
46
+ svelte: 'var(--color-error)',
47
+ markdown: 'var(--color-blue)',
48
+ yaml: 'var(--color-purple)',
49
+ xml: 'var(--color-orange)',
50
+ shell: 'var(--color-success)',
51
+ sass: 'var(--color-error)',
52
+ settings: 'var(--color-lavender)',
53
+ default: 'var(--color-lavender)',
54
54
  }
55
55
 
56
56
  // ═══════════════════════════════════════════════════════════════════════════════