@deepseek-ai/dsh-client-ui-primitives 0.1.7-alpha.1 → 0.1.7-alpha.2
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/README.i18n.yaml +2 -2
- package/README.md +7 -3
- package/README.zh.md +7 -3
- package/lib/CodeCard.module.css +83 -0
- package/lib/DiffBlock.module.css +24 -53
- package/lib/ReadBlock.module.css +13 -81
- package/lib/index.js +193 -122
- package/lib/markdown/CodeBlock.module.css +15 -0
- package/lib/markdown/MarkdownText.module.css +0 -18
- package/lib/types/CodeToolbar.d.ts +30 -0
- package/lib/types/DiffBlock.d.ts +3 -3
- package/lib/types/Menu.d.ts +3 -3
- package/lib/types/ReadBlock.d.ts +2 -1
- package/lib/types/Tooltip.d.ts +2 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/markdown/CodeBlock.d.ts +6 -1
- package/lib/types/markdown/render.d.ts +3 -0
- package/lib/types/user-text.d.ts +2 -2
- package/lib/user-text.module.css +4 -1
- package/package.json +5 -5
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 332845471056e718d49d79a077b76a02c79bafe2
|
|
6
|
+
README.zh.md: 598b561e79e4a935f7f033002e68b91c3a3d84de
|
package/README.md
CHANGED
|
@@ -77,17 +77,21 @@ Writing your own component in your own package is fine when the need is genuinel
|
|
|
77
77
|
|
|
78
78
|
### Controls and icons
|
|
79
79
|
|
|
80
|
-
The catalog above lists what each export is for; this section covers the behavior that props alone do not show. Product icon names omit artboard sizes and end in `Regular` for the supplied one-pixel artwork or `Medium` for the same geometry at a 1.3px stroke; the `size` prop controls rendered dimensions ([decision](../../../.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.md)). Each product, reference, link, and permission glyph intentionally keeps both weight exports even when the current product uses only one, so callers can choose emphasis without adding another API later; fill-only pairs render identically. `IconWarningOutlineRegular`/`Medium` use a circle; `IconWarningTriangleOutlineRegular`/`Medium` use a rounded triangle. `FishLogo` and `BrandWordmark` fill brand slots. `FileTypeIcon` renders the traditional 28px spreadsheet, folder, HTML, image, Markdown, generic, PDF, PPT, video, and Word glyphs, and uses the imported square technology artwork for the established 48 code and configuration categories. The import replaces artwork only: archive-only categories do not extend `CodeFileType`. `classifyFileType` applies exact filename, prefix, suffix, optional project-context, and extension rules in that order; React names win over TypeScript/JavaScript, Angular suffixes win over their base extension, and a Dart file becomes Flutter only when the supplied project files contain a `pubspec.yaml` with `flutter:`. Markdown and SVG remain traditional Markdown and image files. Spreadsheet mappings include CSV, TSV, Excel workbooks and templates, OpenDocument spreadsheets, and Numbers; KEY maps to slides, while RTF/ODT/Pages map to documents. `fileExtension` exposes the same basename and final-dot parsing for adjacent metadata labels. Traditional glyphs use a solid category-colored sheet with a white mark and translucent white corner; the generic file uses a grey sheet and darker grey corner. Callers may override the sheet color through `--dsh-file-type-icon-color`. The full-color technology artwork is the deliberate exception and retains its embedded palette. All glyphs are decorative and carry no label. `LinkIconMedium` is the leading glyph for clickable artifact links — globe, folder, code, image, document, or plain paper, and for a `url` link the mark of a well-known site named by its `href` — the developer sites a transcript usually cites (GitHub, GitLab, npm, PyPI, Stack Overflow, MDN, Wikipedia, Hacker News, YouTube, X, Bilibili, Zhihu, Juejin, CSDN) and mainstream search, video, social, shopping, and reference sites (Google, Baidu, DuckDuckGo, TikTok, Netflix, Spotify, Facebook, Instagram, Reddit, Telegram, WhatsApp, WeChat, QQ, Weibo, Taobao, AliExpress, eBay, Quora, V2EX, Apple) — while `classifyLinkPath` folds the shared file types into that existing six-category vocabulary. `ConnectionIndicator` renders a warning-colored disconnected action whose permanent retry glyph marks the retry action beside the owner-supplied outage label, the shared ongoing loader beside a label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Clicking either warning state requests an immediate reconnect; no hover interaction changes the copy. The pill fades in on appearance, fades out for 150ms before unmounting, and sizes to its current label. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor; `useAnchoredMaxHeight` and portalled `Menu` widen their 12px top viewport margin to the frame's published `--dsh-frame-top-clearance`. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. Its `preview` variant uses the anchor or `widthAnchorRef` element’s width minus 48px, centers the panel with 24px side insets, and places it above or below the row within the viewport. The panel keeps the frame’s top clearance, caps its height at 420px, and follows content and anchor resizing; Escape or pointer and keyboard activation of the anchor dismisses it. The whole preview fades in and out over 100ms; a closing preview stops receiving pointer input and unmounts after the fade. Re-entering the anchor during the fade restores it. Reduced-motion preferences suppress the transition. Copy labels are required only when copying is enabled. `Toast` uses the owner's `holdMs` for both the fade delay and its hold-and-fade lifetime. Rerenders with unchanged `holdMs` do not restart that lifetime; completion uses the latest callback, and fully faded actions cannot receive input. A new component key restarts the banner. `rankByName` is the `/` menu's shared candidate ranker for the command and skill sources: the query must be a case-insensitive ordered subsequence of the name; prefix hits rank first, then alignment score, then source order. `Menu.autoFocus` focuses its first enabled item, supports Arrow Up/Down and Home/End navigation, and focuses the first button in the anchor on Escape; it is opt-in for action menus.
|
|
80
|
+
The catalog above lists what each export is for; this section covers the behavior that props alone do not show. Product icon names omit artboard sizes and end in `Regular` for the supplied one-pixel artwork or `Medium` for the same geometry at a 1.3px stroke; the `size` prop controls rendered dimensions ([decision](../../../.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.md)). Each product, reference, link, and permission glyph intentionally keeps both weight exports even when the current product uses only one, so callers can choose emphasis without adding another API later; fill-only pairs render identically. `IconWarningOutlineRegular`/`Medium` use a circle; `IconWarningTriangleOutlineRegular`/`Medium` use a rounded triangle. `FishLogo` and `BrandWordmark` fill brand slots. `FileTypeIcon` renders the traditional 28px spreadsheet, folder, HTML, image, Markdown, generic, PDF, PPT, video, and Word glyphs, and uses the imported square technology artwork for the established 48 code and configuration categories. The import replaces artwork only: archive-only categories do not extend `CodeFileType`. `classifyFileType` applies exact filename, prefix, suffix, optional project-context, and extension rules in that order; React names win over TypeScript/JavaScript, Angular suffixes win over their base extension, and a Dart file becomes Flutter only when the supplied project files contain a `pubspec.yaml` with `flutter:`. Markdown and SVG remain traditional Markdown and image files. Spreadsheet mappings include CSV, TSV, Excel workbooks and templates, OpenDocument spreadsheets, and Numbers; KEY maps to slides, while RTF/ODT/Pages map to documents. `fileExtension` exposes the same basename and final-dot parsing for adjacent metadata labels. Traditional glyphs use a solid category-colored sheet with a white mark and translucent white corner; the generic file uses a grey sheet and darker grey corner. Callers may override the sheet color through `--dsh-file-type-icon-color`. The full-color technology artwork is the deliberate exception and retains its embedded palette. All glyphs are decorative and carry no label. `LinkIconMedium` is the leading glyph for clickable artifact links — globe, folder, code, image, document, or plain paper, and for a `url` link the mark of a well-known site named by its `href` — the developer sites a transcript usually cites (GitHub, GitLab, npm, PyPI, Stack Overflow, MDN, Wikipedia, Hacker News, YouTube, X, Bilibili, Zhihu, Juejin, CSDN) and mainstream search, video, social, shopping, and reference sites (Google, Baidu, DuckDuckGo, TikTok, Netflix, Spotify, Facebook, Instagram, Reddit, Telegram, WhatsApp, WeChat, QQ, Weibo, Taobao, AliExpress, eBay, Quora, V2EX, Apple) — while `classifyLinkPath` folds the shared file types into that existing six-category vocabulary. `ConnectionIndicator` renders a warning-colored disconnected action whose permanent retry glyph marks the retry action beside the owner-supplied outage label, the shared ongoing loader beside a label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Clicking either warning state requests an immediate reconnect; no hover interaction changes the copy. The pill fades in on appearance, fades out for 150ms before unmounting, and sizes to its current label. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor; `useAnchoredMaxHeight` and portalled `Menu` widen their 12px top viewport margin to the frame's published `--dsh-frame-top-clearance`. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. Its `preview` variant uses the anchor or `widthAnchorRef` element’s width minus 48px, centers the panel with 24px side insets, and places it above or below the row within the viewport. The panel keeps the frame’s top clearance, caps its height at 420px, and follows content and anchor resizing; Escape or pointer and keyboard activation of the anchor dismisses it. The whole preview fades in and out over 100ms; a closing preview stops receiving pointer input and unmounts after the fade. Re-entering the anchor during the fade restores it. Reduced-motion preferences suppress the transition. Copy labels are required only when copying is enabled. `Toast` uses the owner's `holdMs` for both the fade delay and its hold-and-fade lifetime. Rerenders with unchanged `holdMs` do not restart that lifetime; completion uses the latest callback, and fully faded actions cannot receive input. A new component key restarts the banner. `rankByName` is the `/` menu's shared candidate ranker for the command and skill sources: the query must be a case-insensitive ordered subsequence of the name; prefix hits rank first, then alignment score, then source order. Portalled `Menu` lists follow their anchor during dragging and CSS transforms, and stop tracking when closed. `Menu.autoFocus` focuses its first enabled item, supports Arrow Up/Down and Home/End navigation, and focuses the first button in the anchor on Escape; it is opt-in for action menus.
|
|
81
|
+
|
|
82
|
+
`Tooltip` reads the anchor on hover or keyboard focus and fits its bubble from `ResizeObserver` border-box sizes. The bubble stays hidden until its first fit, slides inside the horizontal viewport margin, and flips above or below only when the opposite side fits. Label-size and viewport changes reuse the anchor coordinates; fitting does not synchronously measure the bubble or trigger a React render.
|
|
81
83
|
|
|
82
84
|
### Rendering agent output
|
|
83
85
|
|
|
84
86
|
The nearest `MarkdownDelegateProvider` supplies optional `openExternalLink` and `openFile` navigation callbacks. Nested providers replace the enclosing capabilities, and callback changes reach already-rendered links without rebuilding Markdown. Its `openFile` makes local Markdown links clickable after settlement. Absolute and workspace-relative paths support percent escapes and `#L24` / `#L24-L30` fragments; ranges open at their first line. Literal `?` and `#` in filenames must be percent-encoded. The tooltip uses the decoded path and supplies the accessible name when the label is empty. The callback receives the decoded path and optional line, while the renderer preserves the label and displays a file icon. Without a callback, local links remain text. URL schemes, queries, unsupported fragments, and malformed destinations never reach the file opener.
|
|
85
87
|
|
|
86
|
-
`MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. A surrounding `MarkdownDelegateProvider` receives sanitized HTTP(S) URLs from ordinary clicks; modified clicks and links outside a provider retain native external-anchor behavior. When the owner passes a `pathImages` vocabulary, image destinations that are local media paths rewrite to displayable URLs on settled renders only (the same streaming gate as file mentions); without a vocabulary, local destinations remain inert alt text. A load or decode failure replaces the image with its authored alt text, or the original destination when alt is empty. Changing the image source permits a fresh load. While a reply streams, it freezes completed blocks, advances a top-level open fence by completed lines, and highlights that fence from saved Shiki grammar state. Completed token lines enter fixed-size React groups, so later chunks reconcile only the growing group; an unchanged fence retains that DOM when the final full parse resolves cross-document syntax. `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `projectUserText` projects sent user text into inline plain runs and reference chips for the message bubble and queue rows. When supplied with `UserTextReferences`, file and skill references become keyboard-accessible preview buttons using the same hover and focus styling as prose file links; the first pointer click can open a preview, while subsequent clicks and existing text selections retain native selection handling. Keyboard activation opens previews even when text is selected.
|
|
88
|
+
`MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. A surrounding `MarkdownDelegateProvider` receives sanitized HTTP(S) URLs from ordinary clicks; modified clicks and links outside a provider retain native external-anchor behavior. When the owner passes a `pathImages` vocabulary, image destinations that are local media paths rewrite to displayable URLs on settled renders only (the same streaming gate as file mentions); without a vocabulary, local destinations remain inert alt text. A load or decode failure replaces the image with its authored alt text, or the original destination when alt is empty. Changing the image source permits a fresh load. While a reply streams, it freezes completed blocks, advances a top-level open fence by completed lines, and highlights that fence from saved Shiki grammar state. Completed token lines enter fixed-size React groups, so later chunks reconcile only the growing group; an unchanged fence retains that DOM when the final full parse resolves cross-document syntax. `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `projectUserText` projects sent user text into inline plain runs and reference chips for the message bubble and queue rows. Reference labels inherit the consumer’s wrapping policy: long names wrap within a bubble, while queue previews retain their single-line layout. When supplied with `UserTextReferences`, file and skill references become keyboard-accessible preview buttons using the same hover and focus styling as prose file links; the first pointer click can open a preview, while subsequent clicks and existing text selections retain native selection handling. Keyboard activation opens previews even when text is selected.
|
|
87
89
|
|
|
88
90
|
`MarkdownText` defaults to `variant="body"`. Use `variant="compact"` for secondary content: its 13px text and 20px line height follow the content-size setting, all heading levels use the same size with weight 600, and paragraphs and lists use tighter spacing. Text, links, and code keep the tertiary color; dotted underlines distinguish links. Code headers scroll with their blocks. Tables and math stay enabled at the surrounding text size and scroll horizontally within the available width. Both variants share the parser and streaming cache.
|
|
89
91
|
|
|
90
|
-
`DiffBlock`
|
|
92
|
+
`CodeBlock.toolbarLabels` enables the shared code-card header and spacing. `ReadBlock` and `DiffBlock` use the same header: tertiary-colored language and filename text, secondary-colored copy and wrap icons, and hover or keyboard-focus tooltips. Missing or unsupported languages use the owner’s localized code-block title; mixed-language diffs use that title too. Code cards share typography and content insets; their headers and bodies use the same background in both themes. Read gutters fit the largest returned line number, and diff fills cover the full scrollable row. The wrap toggle keeps a fixed accessible name and exposes its state through `aria-pressed`; its tooltip describes the next action. Wrapping is local to each mounted card and never changes copied text; code fences start wrapped, while reads and diffs preserve source columns until wrapping is enabled. Compact Markdown shares this toolbar, spacing, and code typography while retaining monochrome text. With `toolbarLabels`, source previews can pass `wrap` to follow their owner’s preference and show only the language and copy icon; owner CSS retains the source layout.
|
|
93
|
+
|
|
94
|
+
`DiffBlock` compares the old and new content by line. It shows actual additions and deletions with up to three neutral context lines on each side, separates distant changes with `⋯`, and excludes shared context from tool-summary totals. The card ends after the diff body. Search stops beyond 256 line additions/deletions per fragment; those fragments display and count the complete old and new contents as a coarse replacement, including shared lines. Copy includes the full displayed diff with its prefixes. A final newline is treated as a terminator; differences only in the presence of a final newline are not displayed.
|
|
91
95
|
|
|
92
96
|
`JsonTree` clamps collapsed strings to `collapsedStringLines` (three by default). Expanded strings show raw text, retain sibling commas, and fit within the window and outer scrolling containers. Resize and ancestor-scroll events update that limit. Row copy feedback updates independently of JSON value rendering; pending clipboard writes cannot update a different row or an unmounted tree.
|
|
93
97
|
|
package/README.zh.md
CHANGED
|
@@ -77,17 +77,21 @@ kind: "package-library"
|
|
|
77
77
|
|
|
78
78
|
### 控件与图标
|
|
79
79
|
|
|
80
|
-
上面的目录说明每个导出的用途;本节讲 props 本身看不出来的行为。产品图标名称不含画板尺寸,以 `Regular` 表示原始 1px 图形,以 `Medium` 表示同一几何的 1.3px 描边;`size` prop 控制渲染尺寸([决定](../../../.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.zh.md))。每个产品、引用、链接与权限图形都会有意保留两种线重导出,即使当前产品只使用其中一种,也让调用方无需再次扩展 API 就能选择强调程度;仅填充的成对图形外观相同。`IconWarningOutlineRegular`/`Medium` 使用圆形;`IconWarningTriangleOutlineRegular`/`Medium` 使用圆角三角形。`FishLogo` 与 `BrandWordmark` 填充品牌 slot。`FileTypeIcon` 渲染传统的 28px spreadsheet、folder、HTML、image、Markdown、generic、PDF、PPT、video 与 Word 图形,并为现有 48 个代码和配置类别使用导入的方形技术图形。该导入只替换图形:资源包中额外的类别不会扩展 `CodeFileType`。`classifyFileType` 按完整文件名、前缀、后缀、可选项目上下文、扩展名的顺序匹配;React 文件名优先于 TypeScript/JavaScript,Angular 后缀优先于基础扩展名,只有传入的项目文件包含带 `flutter:` 的 `pubspec.yaml` 时 Dart 文件才使用 Flutter。Markdown 与 SVG 仍分别使用传统 Markdown 与图片图形。表格映射包括 CSV、TSV、Excel 工作簿与模板、OpenDocument 表格和 Numbers;KEY 映射为幻灯片,RTF/ODT/Pages 映射为文档。`fileExtension` 为相邻元数据 label 暴露同一套 basename 与最终点号解析。传统图形使用实色分类底板、白色标记和半透明白色折角;通用文件使用灰色底板与较深灰色折角。调用方可通过 `--dsh-file-type-icon-color` 覆盖底板颜色。全彩技术图形是明确例外,会保留其内嵌调色板。所有图形都是装饰性的,不自带 label。`LinkIconMedium` 是可点击产物链接的前置图形——地球、文件夹、代码、图片、文档或纸张,`url` 链接的 `href` 指向已知站点时则改用该站点自己的标记——转写内容常引用的开发者站点(GitHub、GitLab、npm、PyPI、Stack Overflow、MDN、Wikipedia、Hacker News、YouTube、X、Bilibili、知乎、掘金、CSDN),以及主流搜索、视频、社交、购物与参考资料站点(Google、百度、DuckDuckGo、TikTok、Netflix、Spotify、Facebook、Instagram、Reddit、Telegram、WhatsApp、微信、QQ、微博、淘宝、速卖通、eBay、Quora、V2EX、Apple)——`classifyLinkPath` 把共享文件类型折叠进原有六类词汇。`ConnectionIndicator` 可渲染警告色的断联操作(常驻重试图形指明重试动作,断联文案由持有方提供)、共享 ongoing loading 加一至三个点以独立于 retry 时序的 500ms 节奏推进的连接中状态,或成功色的恢复状态。点击任一警告状态都会请求立即重连;没有任何悬停交互会改变文案。药丸出现时淡入、卸载前淡出 150ms,宽度随当前 label 自适应。它的持有方提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点;`useAnchoredMaxHeight` 与 portal 模式的 `Menu` 会把 12px 的视口顶部边距加宽到框架发布的 `--dsh-frame-top-clearance`。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。其 `preview` 变体使用 anchor 或 `widthAnchorRef` 元素的宽度减去 48px,左右各内缩 24px,并在视口内放置于行的上方或下方。浮层避开框架顶部保留区,高度最多 420px,会跟随内容及 anchor 尺寸变化;Escape 或通过鼠标和键盘激活锚点可将其关闭。整个浮层的淡入和淡出各持续 100ms;关闭中的浮层停止接收指针输入,淡出后卸载。在淡出期间移回锚点可恢复显示。减少动态效果偏好会禁用过渡动画。只有启用复制时才必须提供复制标签。`Toast` 使用调用方的 `holdMs` 同时控制淡出延迟与停留加淡出的总时长。`holdMs` 未变时,父组件重渲染不会重启该生命周期;完成时调用最新回调,完全淡出的操作不能接收输入。新的组件 key 会重新开始横幅周期。`rankByName` 是 `/` 菜单命令源与 skill(技能)源共享的候选排序器:查询必须是名字的不区分大小写的有序子序列;前缀命中排最前,其次按对齐分数,再按来源顺序。 `Menu.autoFocus` 聚焦首个启用项,支持上下方向键与 Home/End 导航,并在 Escape 时聚焦 anchor 内的第一个按钮;操作菜单可显式启用。
|
|
80
|
+
上面的目录说明每个导出的用途;本节讲 props 本身看不出来的行为。产品图标名称不含画板尺寸,以 `Regular` 表示原始 1px 图形,以 `Medium` 表示同一几何的 1.3px 描边;`size` prop 控制渲染尺寸([决定](../../../.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.zh.md))。每个产品、引用、链接与权限图形都会有意保留两种线重导出,即使当前产品只使用其中一种,也让调用方无需再次扩展 API 就能选择强调程度;仅填充的成对图形外观相同。`IconWarningOutlineRegular`/`Medium` 使用圆形;`IconWarningTriangleOutlineRegular`/`Medium` 使用圆角三角形。`FishLogo` 与 `BrandWordmark` 填充品牌 slot。`FileTypeIcon` 渲染传统的 28px spreadsheet、folder、HTML、image、Markdown、generic、PDF、PPT、video 与 Word 图形,并为现有 48 个代码和配置类别使用导入的方形技术图形。该导入只替换图形:资源包中额外的类别不会扩展 `CodeFileType`。`classifyFileType` 按完整文件名、前缀、后缀、可选项目上下文、扩展名的顺序匹配;React 文件名优先于 TypeScript/JavaScript,Angular 后缀优先于基础扩展名,只有传入的项目文件包含带 `flutter:` 的 `pubspec.yaml` 时 Dart 文件才使用 Flutter。Markdown 与 SVG 仍分别使用传统 Markdown 与图片图形。表格映射包括 CSV、TSV、Excel 工作簿与模板、OpenDocument 表格和 Numbers;KEY 映射为幻灯片,RTF/ODT/Pages 映射为文档。`fileExtension` 为相邻元数据 label 暴露同一套 basename 与最终点号解析。传统图形使用实色分类底板、白色标记和半透明白色折角;通用文件使用灰色底板与较深灰色折角。调用方可通过 `--dsh-file-type-icon-color` 覆盖底板颜色。全彩技术图形是明确例外,会保留其内嵌调色板。所有图形都是装饰性的,不自带 label。`LinkIconMedium` 是可点击产物链接的前置图形——地球、文件夹、代码、图片、文档或纸张,`url` 链接的 `href` 指向已知站点时则改用该站点自己的标记——转写内容常引用的开发者站点(GitHub、GitLab、npm、PyPI、Stack Overflow、MDN、Wikipedia、Hacker News、YouTube、X、Bilibili、知乎、掘金、CSDN),以及主流搜索、视频、社交、购物与参考资料站点(Google、百度、DuckDuckGo、TikTok、Netflix、Spotify、Facebook、Instagram、Reddit、Telegram、WhatsApp、微信、QQ、微博、淘宝、速卖通、eBay、Quora、V2EX、Apple)——`classifyLinkPath` 把共享文件类型折叠进原有六类词汇。`ConnectionIndicator` 可渲染警告色的断联操作(常驻重试图形指明重试动作,断联文案由持有方提供)、共享 ongoing loading 加一至三个点以独立于 retry 时序的 500ms 节奏推进的连接中状态,或成功色的恢复状态。点击任一警告状态都会请求立即重连;没有任何悬停交互会改变文案。药丸出现时淡入、卸载前淡出 150ms,宽度随当前 label 自适应。它的持有方提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点;`useAnchoredMaxHeight` 与 portal 模式的 `Menu` 会把 12px 的视口顶部边距加宽到框架发布的 `--dsh-frame-top-clearance`。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。其 `preview` 变体使用 anchor 或 `widthAnchorRef` 元素的宽度减去 48px,左右各内缩 24px,并在视口内放置于行的上方或下方。浮层避开框架顶部保留区,高度最多 420px,会跟随内容及 anchor 尺寸变化;Escape 或通过鼠标和键盘激活锚点可将其关闭。整个浮层的淡入和淡出各持续 100ms;关闭中的浮层停止接收指针输入,淡出后卸载。在淡出期间移回锚点可恢复显示。减少动态效果偏好会禁用过渡动画。只有启用复制时才必须提供复制标签。`Toast` 使用调用方的 `holdMs` 同时控制淡出延迟与停留加淡出的总时长。`holdMs` 未变时,父组件重渲染不会重启该生命周期;完成时调用最新回调,完全淡出的操作不能接收输入。新的组件 key 会重新开始横幅周期。`rankByName` 是 `/` 菜单命令源与 skill(技能)源共享的候选排序器:查询必须是名字的不区分大小写的有序子序列;前缀命中排最前,其次按对齐分数,再按来源顺序。 portal 模式的 `Menu` 列表会在拖动和 CSS 变换期间跟随锚点,并在关闭时停止跟踪。`Menu.autoFocus` 聚焦首个启用项,支持上下方向键与 Home/End 导航,并在 Escape 时聚焦 anchor 内的第一个按钮;操作菜单可显式启用。
|
|
81
|
+
|
|
82
|
+
`Tooltip` 在悬停或键盘聚焦时读取锚点位置,再根据 `ResizeObserver` 提供的边框盒尺寸调整气泡。首次定位前气泡保持隐藏;横向移入视口留白,仅在另一侧容得下时上下翻转。标签尺寸与视口变化复用锚点坐标;定位不会同步测量气泡,也不会触发 React 渲染。
|
|
81
83
|
|
|
82
84
|
### 渲染 agent 输出
|
|
83
85
|
|
|
84
86
|
最近的 `MarkdownDelegateProvider` 提供可选的 `openExternalLink` 和 `openFile` 导航回调。嵌套 Provider 替换外层能力,回调变化无需重新构建 Markdown 即可到达已渲染链接。其 `openFile` 使本地 Markdown 链接在落定后可点击。绝对路径和工作区相对路径支持百分号转义以及 `#L24` / `#L24-L30` 片段;范围定位到起始行。文件名中的字面 `?` 和 `#` 必须百分号编码。悬停提示使用解码后的路径,并在标签为空时提供可访问名称。回调接收解码后的路径和可选行号,渲染器保留标签并显示文件图标。不传回调时,本地链接仍为文本。URL 协议、查询串、不支持的片段及格式错误的目标不会传给文件打开器。
|
|
85
87
|
|
|
86
|
-
`MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。外层 `MarkdownDelegateProvider` 会接收普通点击产生的已净化 HTTP(S) URL;带修饰键的点击和 Provider 外的链接保留原生外部 anchor 行为。当 owner 传入 `pathImages` 词表时,本地媒体路径的图片目标只在落定渲染阶段重写为可展示 URL(与 file mentions 相同的流式门);不传词表时本地目标保持惰性 alt 文本。加载或解码失败后,图片替换为作者的 alt 文本;alt 为空时显示原始目标路径。图片源变化后可重新加载。回复流式输出时,它冻结已完成的块、按已完成行推进顶层未闭合 fence,并从保存的 Shiki grammar state 为该 fence 增量高亮。已完成的 token 行进入固定大小的 React 分组,后续分片只 reconcile 正在增长的分组;最终全量解析解决跨文档语法时,未变化的 fence 会保留该 DOM。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`projectUserText` 把已发送的用户文本投影为行内普通文本段与引用 chip
|
|
88
|
+
`MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。外层 `MarkdownDelegateProvider` 会接收普通点击产生的已净化 HTTP(S) URL;带修饰键的点击和 Provider 外的链接保留原生外部 anchor 行为。当 owner 传入 `pathImages` 词表时,本地媒体路径的图片目标只在落定渲染阶段重写为可展示 URL(与 file mentions 相同的流式门);不传词表时本地目标保持惰性 alt 文本。加载或解码失败后,图片替换为作者的 alt 文本;alt 为空时显示原始目标路径。图片源变化后可重新加载。回复流式输出时,它冻结已完成的块、按已完成行推进顶层未闭合 fence,并从保存的 Shiki grammar state 为该 fence 增量高亮。已完成的 token 行进入固定大小的 React 分组,后续分片只 reconcile 正在增长的分组;最终全量解析解决跨文档语法时,未变化的 fence 会保留该 DOM。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`projectUserText` 把已发送的用户文本投影为行内普通文本段与引用 chip,供消息气泡和排队行使用。引用名称继承调用方的换行规则:长名称在气泡内换行,排队预览保持单行布局。传入 `UserTextReferences` 时,文件和 skill 引用成为支持键盘操作的预览按钮,复用正文文件链接的悬停和聚焦样式;第一次指针点击可以打开预览,后续点击和已有选区保留原生选择行为。键盘激活在存在选区时仍可打开预览。
|
|
87
89
|
|
|
88
90
|
`MarkdownText` 默认为 `variant="body"`。次级内容使用 `variant="compact"`:其 13px 字号与 20px 行高跟随内容字号设置,各级标题保持同一字号并使用 600 字重,段落与列表采用更紧凑的间距。正文、链接和代码均保持 tertiary 颜色,以点状下划线区分链接。代码标题栏随代码块滚动。表格和公式仍然启用,使用周围文字的字号,并在可用宽度内横向滚动。两个变体共享解析器与流式缓存。
|
|
89
91
|
|
|
90
|
-
`DiffBlock`
|
|
92
|
+
`CodeBlock.toolbarLabels` 启用共享代码卡片的标题栏和间距。`ReadBlock` 与 `DiffBlock` 使用相同的标题栏:tertiary 色的语言名称与文件名、secondary 色的复制与换行图标,以及悬停或键盘聚焦时的提示。语言缺失或不受支持时显示调用方提供的本地化代码块标题;混合语言的 Diff 也使用该标题。代码卡片共用字体和内容内边距;在两种主题下,标题栏和正文都使用相同背景。读取卡片的行号栏容纳返回窗口中的最大行号,Diff 底色覆盖完整可滚动行。换行按钮保留固定的无障碍名称,以 `aria-pressed` 表达状态;提示文本说明下一步操作。换行设置仅属于当前挂载的卡片,不改变复制的文本;代码围栏默认换行,读取和差异卡片则在启用换行前保留源码列位置。紧凑 Markdown 共用此工具栏、间距和代码字体排版,同时保留单色文本。源码预览传入 `toolbarLabels` 后,可用 `wrap` 跟随所属视图的换行设置,仅显示语言和复制图标;所属视图的 CSS 保留源码布局。
|
|
93
|
+
|
|
94
|
+
`DiffBlock` 按行比较新旧内容。它显示实际增删行及两侧最多三行中性上下文,用 `⋯` 分隔远距离改动,工具摘要的统计不计入共享上下文。卡片在差异正文后结束。若一个片段需要超过 256 次行新增或删除,则停止精确比较;该片段按完整新旧内容显示和统计为粗粒度替换,包含共享行。复制包含完整显示 diff 及其前缀。末尾换行视为行终止符;仅末尾换行不同不会显示为改动。
|
|
91
95
|
|
|
92
96
|
`JsonTree` 把折叠字符串限制为 `collapsedStringLines` 行(默认三行)。展开后显示原始文本、保留同级逗号,并限制在窗口与外层滚动容器内;尺寸变化和祖先滚动事件会更新此限制。行复制反馈独立于 JSON 值渲染更新;尚未完成的剪贴板写入不会更新另一行或已卸载的树。
|
|
93
97
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
.card {
|
|
2
|
+
position: relative;
|
|
3
|
+
margin: 16px 0;
|
|
4
|
+
color: var(--dsw-alias-label-primary);
|
|
5
|
+
background: var(--dsw-alias-markdown-code-block);
|
|
6
|
+
border-radius: 12px;
|
|
7
|
+
font: var(--dsw-font-markdown-code-block);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.header {
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
justify-content: space-between;
|
|
14
|
+
gap: 12px;
|
|
15
|
+
padding: 10px 18px 8px 22px;
|
|
16
|
+
background: var(--dsl-code-block-background, var(--dsw-alias-markdown-code-block));
|
|
17
|
+
border-top-left-radius: inherit;
|
|
18
|
+
border-top-right-radius: inherit;
|
|
19
|
+
color: var(--dsw-alias-label-secondary);
|
|
20
|
+
font: 11px/18px var(--dsw-font-family);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.heading {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: baseline;
|
|
26
|
+
gap: 12px;
|
|
27
|
+
min-width: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.language {
|
|
31
|
+
color: var(--dsw-alias-label-tertiary);
|
|
32
|
+
flex: none;
|
|
33
|
+
font-family: var(--ds-font-family-code);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.title {
|
|
37
|
+
color: var(--dsw-alias-label-tertiary);
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
text-overflow: ellipsis;
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.actions {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 4px;
|
|
47
|
+
flex: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.status {
|
|
51
|
+
margin-right: 8px;
|
|
52
|
+
color: var(--dsw-alias-label-tertiary);
|
|
53
|
+
white-space: nowrap;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.action {
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
width: 24px;
|
|
61
|
+
height: 24px;
|
|
62
|
+
padding: 0;
|
|
63
|
+
border: 0;
|
|
64
|
+
border-radius: 6px;
|
|
65
|
+
color: var(--dsw-alias-label-secondary);
|
|
66
|
+
background: transparent;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.action:hover {
|
|
71
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.action:focus-visible {
|
|
75
|
+
outline: 1px solid var(--dsw-alias-state-business-primary);
|
|
76
|
+
outline-offset: 2px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.body {
|
|
80
|
+
padding: 6px 22px 20px;
|
|
81
|
+
font: var(--dsw-font-markdown-code-block);
|
|
82
|
+
overflow-x: auto;
|
|
83
|
+
}
|
package/lib/DiffBlock.module.css
CHANGED
|
@@ -1,79 +1,58 @@
|
|
|
1
1
|
.block {
|
|
2
|
-
|
|
3
|
-
--dsl-diff-line-height: 22px;
|
|
4
|
-
|
|
5
|
-
position: relative;
|
|
6
|
-
margin: 16px 0;
|
|
7
|
-
color: var(--dsw-alias-label-primary);
|
|
8
|
-
background: var(--dsw-alias-markdown-code-block);
|
|
9
|
-
border-radius: var(--dsl-diff-radius);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/* The copy control floats in the top-right corner over the body, so the card
|
|
13
|
-
has no empty banner row above its first diff line (the TUI diff card has no
|
|
14
|
-
banner either — only the footer). The block is position: relative, so this
|
|
15
|
-
anchors to the card. */
|
|
16
|
-
.copyButton {
|
|
17
|
-
position: absolute;
|
|
18
|
-
top: 8px;
|
|
19
|
-
right: 12px;
|
|
20
|
-
z-index: 1;
|
|
21
|
-
background-color: transparent;
|
|
22
|
-
border: none;
|
|
23
|
-
padding: 0;
|
|
24
|
-
margin: 0;
|
|
25
|
-
color: var(--dsw-alias-label-secondary);
|
|
26
|
-
cursor: pointer;
|
|
27
|
-
font: var(--dsw-font-xs-13);
|
|
2
|
+
overflow: hidden;
|
|
28
3
|
}
|
|
29
4
|
|
|
30
5
|
.body {
|
|
31
|
-
|
|
6
|
+
display: grid;
|
|
7
|
+
grid-template-columns: minmax(max-content, 1fr);
|
|
8
|
+
padding: 6px 0 20px;
|
|
32
9
|
font: var(--dsw-font-markdown-code-block);
|
|
33
10
|
overflow-x: auto;
|
|
34
|
-
overflow-y: hidden;
|
|
35
11
|
}
|
|
36
12
|
|
|
37
|
-
/* No wrapping, no word-break: a diff is read by its indentation. */
|
|
38
13
|
.line {
|
|
39
|
-
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
min-height: 1lh;
|
|
16
|
+
padding: 0 22px;
|
|
40
17
|
white-space: pre;
|
|
41
18
|
}
|
|
42
19
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
20
|
+
.block[data-code-wrap='true'] .body {
|
|
21
|
+
grid-template-columns: minmax(0, 1fr);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.block[data-code-wrap='true'] .line {
|
|
25
|
+
white-space: pre-wrap;
|
|
26
|
+
overflow-wrap: anywhere;
|
|
27
|
+
}
|
|
28
|
+
|
|
47
29
|
.path {
|
|
48
|
-
color: var(--dsw-alias-label-
|
|
49
|
-
font-weight:
|
|
50
|
-
padding-right: 56px;
|
|
30
|
+
color: var(--dsw-alias-label-secondary);
|
|
31
|
+
font-weight: 500;
|
|
51
32
|
}
|
|
52
33
|
|
|
53
|
-
/* A same-file second hunk's separator (a scattered edit), in the dim tone. */
|
|
54
34
|
.gap {
|
|
55
35
|
color: var(--dsw-alias-label-tertiary);
|
|
56
36
|
}
|
|
57
37
|
|
|
58
|
-
/* The diff's own meaning-carrying colors: removed on the error token, added on
|
|
59
|
-
the success token. A `- `/`+ ` prefix is drawn here so a copied line and the
|
|
60
|
-
shown line agree, and so the sign reads without relying on color alone. */
|
|
61
38
|
.del::before {
|
|
62
39
|
content: '- ';
|
|
63
|
-
color: var(--dsw-alias-state-error-primary);
|
|
64
40
|
}
|
|
65
41
|
|
|
66
42
|
.del {
|
|
67
43
|
color: var(--dsw-alias-state-error-primary);
|
|
44
|
+
background: var(--dsw-alias-code-diff-deleted);
|
|
45
|
+
box-shadow: inset 3px 0 0 var(--dsw-alias-state-error-primary);
|
|
68
46
|
}
|
|
69
47
|
|
|
70
48
|
.add::before {
|
|
71
49
|
content: '+ ';
|
|
72
|
-
color: var(--dsw-alias-state-success-primary);
|
|
73
50
|
}
|
|
74
51
|
|
|
75
52
|
.add {
|
|
76
53
|
color: var(--dsw-alias-state-success-primary);
|
|
54
|
+
background: var(--dsw-alias-code-diff-added);
|
|
55
|
+
box-shadow: inset 3px 0 0 var(--dsw-alias-state-success-primary);
|
|
77
56
|
}
|
|
78
57
|
|
|
79
58
|
.context {
|
|
@@ -87,9 +66,9 @@
|
|
|
87
66
|
.expand {
|
|
88
67
|
display: block;
|
|
89
68
|
width: 100%;
|
|
90
|
-
padding: 0;
|
|
69
|
+
padding: 0 22px;
|
|
91
70
|
border: none;
|
|
92
|
-
background
|
|
71
|
+
background: transparent;
|
|
93
72
|
color: var(--dsw-alias-label-tertiary);
|
|
94
73
|
cursor: pointer;
|
|
95
74
|
font: inherit;
|
|
@@ -99,11 +78,3 @@
|
|
|
99
78
|
.expand:hover {
|
|
100
79
|
color: var(--dsw-alias-label-secondary);
|
|
101
80
|
}
|
|
102
|
-
|
|
103
|
-
/* The change summary, dim under the body: `└ +A -R · N file(s)`, the same
|
|
104
|
-
footer the TUI transcript's diff card draws. */
|
|
105
|
-
.footer {
|
|
106
|
-
padding: 0 14px 12px;
|
|
107
|
-
font: var(--dsw-font-markdown-code-block);
|
|
108
|
-
color: var(--dsw-alias-label-tertiary);
|
|
109
|
-
}
|
package/lib/ReadBlock.module.css
CHANGED
|
@@ -1,105 +1,37 @@
|
|
|
1
|
-
.block {
|
|
2
|
-
--dsl-read-radius: 12px;
|
|
3
|
-
--dsl-read-line-height: 22px;
|
|
4
|
-
/* Fixed-width gutter column for the line numbers, so the content edge stays
|
|
5
|
-
put down the whole window regardless of how wide the numbers grow. */
|
|
6
|
-
--dsl-read-gutter: 48px;
|
|
7
|
-
|
|
8
|
-
position: relative;
|
|
9
|
-
margin: 16px 0;
|
|
10
|
-
color: var(--dsw-alias-label-primary);
|
|
11
|
-
background: var(--dsw-alias-markdown-code-block);
|
|
12
|
-
border-radius: var(--dsl-read-radius);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.banner {
|
|
16
|
-
display: flex;
|
|
17
|
-
justify-content: space-between;
|
|
18
|
-
align-items: center;
|
|
19
|
-
gap: 12px;
|
|
20
|
-
padding: 9px 14px;
|
|
21
|
-
background: var(--dsw-alias-markdown-code-block-banner);
|
|
22
|
-
border-top-left-radius: var(--dsl-read-radius);
|
|
23
|
-
border-top-right-radius: var(--dsl-read-radius);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.label {
|
|
27
|
-
min-width: 0;
|
|
28
|
-
overflow: hidden;
|
|
29
|
-
text-overflow: ellipsis;
|
|
30
|
-
white-space: nowrap;
|
|
31
|
-
color: var(--dsw-alias-label-primary);
|
|
32
|
-
font-family: var(--ds-font-family-code);
|
|
33
|
-
font-size: 12px;
|
|
34
|
-
line-height: 18px;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.action {
|
|
38
|
-
display: flex;
|
|
39
|
-
align-items: center;
|
|
40
|
-
flex-shrink: 0;
|
|
41
|
-
gap: 12px;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.count {
|
|
45
|
-
color: var(--dsw-alias-label-tertiary);
|
|
46
|
-
font: var(--dsw-font-xs-13);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.lang {
|
|
50
|
-
color: var(--dsw-alias-label-tertiary);
|
|
51
|
-
font-family: var(--ds-font-family-code);
|
|
52
|
-
font-size: 12px;
|
|
53
|
-
line-height: 18px;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.copyButton {
|
|
57
|
-
background-color: transparent;
|
|
58
|
-
border: none;
|
|
59
|
-
padding: 0;
|
|
60
|
-
margin: 0;
|
|
61
|
-
color: var(--dsw-alias-label-secondary);
|
|
62
|
-
cursor: pointer;
|
|
63
|
-
font: var(--dsw-font-xs-13);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.body {
|
|
67
|
-
padding: 12px 0;
|
|
68
|
-
font: var(--dsw-font-markdown-code-block);
|
|
69
|
-
overflow-x: auto;
|
|
70
|
-
overflow-y: hidden;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/* One row per file line: a fixed gutter column, then the content. No wrapping —
|
|
74
|
-
a source line's leading whitespace is meaningful and scrolls sideways. */
|
|
75
1
|
.line {
|
|
76
2
|
display: flex;
|
|
77
|
-
min-height:
|
|
78
|
-
line-height: var(--dsl-read-line-height);
|
|
3
|
+
min-height: 1lh;
|
|
79
4
|
white-space: pre;
|
|
80
5
|
}
|
|
81
6
|
|
|
82
7
|
.gutter {
|
|
83
8
|
flex: none;
|
|
84
9
|
width: var(--dsl-read-gutter);
|
|
85
|
-
|
|
10
|
+
margin-right: 12px;
|
|
86
11
|
text-align: right;
|
|
87
12
|
color: var(--dsw-alias-label-tertiary);
|
|
88
|
-
/* The gutter is chrome, not content: keep it out of a text selection so a
|
|
89
|
-
copy of the visible rows carries the source, not the line numbers. */
|
|
90
13
|
user-select: none;
|
|
91
14
|
}
|
|
92
15
|
|
|
93
16
|
.content {
|
|
17
|
+
min-width: 0;
|
|
94
18
|
color: var(--dsw-alias-label-primary);
|
|
95
19
|
}
|
|
96
20
|
|
|
21
|
+
.block[data-code-wrap='true'] .line {
|
|
22
|
+
white-space: pre-wrap;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.block[data-code-wrap='true'] .content {
|
|
26
|
+
overflow-wrap: anywhere;
|
|
27
|
+
}
|
|
28
|
+
|
|
97
29
|
.expand {
|
|
98
30
|
display: block;
|
|
99
31
|
width: 100%;
|
|
100
|
-
padding: 0 0 0 var(--dsl-read-gutter);
|
|
32
|
+
padding: 0 0 0 calc(var(--dsl-read-gutter) + 12px);
|
|
101
33
|
border: none;
|
|
102
|
-
background
|
|
34
|
+
background: transparent;
|
|
103
35
|
color: var(--dsw-alias-label-tertiary);
|
|
104
36
|
cursor: pointer;
|
|
105
37
|
font: inherit;
|
package/lib/index.js
CHANGED
|
@@ -38,6 +38,7 @@ import langJson from "@shikijs/langs/json";
|
|
|
38
38
|
import css$24 from "./JsonTree.module.css";
|
|
39
39
|
import Anser from "anser";
|
|
40
40
|
import css$25 from "./TerminalBlock.module.css";
|
|
41
|
+
import cardCss from "./CodeCard.module.css";
|
|
41
42
|
import css$26 from "./ReadBlock.module.css";
|
|
42
43
|
import { structuredPatch } from "diff";
|
|
43
44
|
import css$27 from "./DiffBlock.module.css";
|
|
@@ -3500,7 +3501,7 @@ const MEASURE_STYLE = {
|
|
|
3500
3501
|
* @param props.align - list alignment against the anchor (default 'start').
|
|
3501
3502
|
* @param props.side - open below (`bottom`, default) or above (`top`) the anchor.
|
|
3502
3503
|
* @param props.portal - render the list into document.body, fixed-positioned
|
|
3503
|
-
* from the anchor rect (
|
|
3504
|
+
* from the anchor rect (follows movement and resizing while open). Use when an
|
|
3504
3505
|
* ancestor's overflow clipping would crop the in-place list; default false
|
|
3505
3506
|
* keeps the pure-CSS in-place behavior.
|
|
3506
3507
|
* @param props.closeOnPointerLeave - close the list once the pointer has left
|
|
@@ -3513,8 +3514,8 @@ const MEASURE_STYLE = {
|
|
|
3513
3514
|
* directly (e.g. from a host-owned trigger button) instead of measuring the
|
|
3514
3515
|
* Menu's own wrapper span. Required when the wrapper isn't itself laid out at
|
|
3515
3516
|
* the trigger (render-prop anchors, effect-positioned proxies — measuring the
|
|
3516
|
-
* wrapper there races the host's layout effects). Called on open
|
|
3517
|
-
* scroll/resize; return null to skip placement for that frame.
|
|
3517
|
+
* wrapper there races the host's layout effects). Called on open, each animation
|
|
3518
|
+
* frame, and scroll/resize; return null to skip placement for that frame.
|
|
3518
3519
|
* @param props.footer - rows pinned below the scrolling items area, separated
|
|
3519
3520
|
* by a hairline; they stay visible while the items above scroll.
|
|
3520
3521
|
* @param props.children - component rows rendered after `items` in the same
|
|
@@ -3607,15 +3608,21 @@ function Menu({ open, anchor, items = [], children, selectedId, selectedIds, onS
|
|
|
3607
3608
|
}
|
|
3608
3609
|
if (lw > 0) x = Math.min(Math.max(x, MARGIN), vw - lw - MARGIN);
|
|
3609
3610
|
if (lh > 0) y = Math.min(Math.max(y, overlayTopMargin(MARGIN)), vh - lh - MARGIN);
|
|
3610
|
-
setFixedPos({
|
|
3611
|
+
setFixedPos((current) => current?.left === x && current.top === y ? current : {
|
|
3611
3612
|
left: x,
|
|
3612
3613
|
top: y
|
|
3613
3614
|
});
|
|
3614
3615
|
};
|
|
3615
3616
|
place();
|
|
3617
|
+
const track = () => {
|
|
3618
|
+
place();
|
|
3619
|
+
frame = requestAnimationFrame(track);
|
|
3620
|
+
};
|
|
3621
|
+
let frame = requestAnimationFrame(track);
|
|
3616
3622
|
window.addEventListener("scroll", place, true);
|
|
3617
3623
|
window.addEventListener("resize", place);
|
|
3618
3624
|
return () => {
|
|
3625
|
+
cancelAnimationFrame(frame);
|
|
3619
3626
|
window.removeEventListener("scroll", place, true);
|
|
3620
3627
|
window.removeEventListener("resize", place);
|
|
3621
3628
|
};
|
|
@@ -5919,7 +5926,8 @@ if (typeof window !== "undefined") {
|
|
|
5919
5926
|
* @param props.maxWidth - bubble width cap in pixels, for labels long enough that the default
|
|
5920
5927
|
* half-viewport cap would render a slab wider than the surface the anchor sits on.
|
|
5921
5928
|
* @param props.children - a single anchor element; its own ref (callback or object) is forwarded alongside the tooltip's.
|
|
5922
|
-
* @returns the cloned anchor plus a fixed-position bubble, optionally portaled to the body
|
|
5929
|
+
* @returns the cloned anchor plus a fixed-position bubble, optionally portaled to the body.
|
|
5930
|
+
* The bubble stays hidden until ResizeObserver supplies its size for viewport fitting; clicking the
|
|
5923
5931
|
* anchor dismisses the bubble until the next trigger, and focus arriving after a pointer
|
|
5924
5932
|
* interaction (a closing menu refocusing its trigger) never raises it.
|
|
5925
5933
|
*/
|
|
@@ -5932,40 +5940,9 @@ function Tooltip({ label, side = "right", align = "center", delayMs = 0, disable
|
|
|
5932
5940
|
else if (childRef != null) childRef.current = el;
|
|
5933
5941
|
}, [childRef]);
|
|
5934
5942
|
const [pos, setPos] = useState(null);
|
|
5935
|
-
const [placement, setPlacement] = useState(side);
|
|
5936
5943
|
const bubble = useRef(null);
|
|
5937
5944
|
const resolvedLabel = pos === null ? null : typeof label === "function" ? label() : label;
|
|
5938
|
-
const y = pos === null ? 0 :
|
|
5939
|
-
const EDGE_MARGIN = 12;
|
|
5940
|
-
useLayoutEffect(() => {
|
|
5941
|
-
if (pos === null) return;
|
|
5942
|
-
const fit = () => {
|
|
5943
|
-
const el = bubble.current;
|
|
5944
|
-
/* v8 ignore next -- pos is set only while the bubble is mounted. */
|
|
5945
|
-
if (el === null) return;
|
|
5946
|
-
el.style.left = `${pos.x}px`;
|
|
5947
|
-
const r = el.getBoundingClientRect();
|
|
5948
|
-
let dx = 0;
|
|
5949
|
-
if (r.right > window.innerWidth - EDGE_MARGIN) dx = window.innerWidth - EDGE_MARGIN - r.right;
|
|
5950
|
-
if (r.left + dx < EDGE_MARGIN) dx = EDGE_MARGIN - r.left;
|
|
5951
|
-
el.style.left = `${pos.x + dx}px`;
|
|
5952
|
-
if (side === "right") return;
|
|
5953
|
-
const fitsBelow = pos.bottom + 8 + r.height <= window.innerHeight - EDGE_MARGIN;
|
|
5954
|
-
const fitsAbove = pos.top - 8 - r.height >= EDGE_MARGIN;
|
|
5955
|
-
if (placement === "bottom" && !fitsBelow && fitsAbove) setPlacement("top");
|
|
5956
|
-
if (placement === "top" && !fitsAbove && fitsBelow) setPlacement("bottom");
|
|
5957
|
-
};
|
|
5958
|
-
fit();
|
|
5959
|
-
window.addEventListener("resize", fit);
|
|
5960
|
-
return () => {
|
|
5961
|
-
window.removeEventListener("resize", fit);
|
|
5962
|
-
};
|
|
5963
|
-
}, [
|
|
5964
|
-
placement,
|
|
5965
|
-
pos,
|
|
5966
|
-
resolvedLabel,
|
|
5967
|
-
side
|
|
5968
|
-
]);
|
|
5945
|
+
const y = pos === null ? 0 : side === "right" ? pos.top + (pos.bottom - pos.top) / 2 : side === "top" ? pos.top - 8 : pos.bottom + 8;
|
|
5969
5946
|
const showTimer = useRef(null);
|
|
5970
5947
|
const triggers = useRef({
|
|
5971
5948
|
hover: false,
|
|
@@ -5977,6 +5954,43 @@ function Tooltip({ label, side = "right", align = "center", delayMs = 0, disable
|
|
|
5977
5954
|
suppressAncestors?.(active);
|
|
5978
5955
|
}, [suppressAncestors]);
|
|
5979
5956
|
const visible = pos !== null && !disabled;
|
|
5957
|
+
useEffect(() => {
|
|
5958
|
+
const el = bubble.current;
|
|
5959
|
+
if (pos === null || !visible || suppressed || el === null) return;
|
|
5960
|
+
const edgeMargin = 12;
|
|
5961
|
+
let size;
|
|
5962
|
+
let placement = side;
|
|
5963
|
+
const fit = () => {
|
|
5964
|
+
if (size === void 0) return;
|
|
5965
|
+
const { inlineSize: width, blockSize: height } = size;
|
|
5966
|
+
const offset = side === "right" ? 0 : align === "end" ? width : width / 2;
|
|
5967
|
+
const left = Math.max(edgeMargin, Math.min(pos.x - offset, window.innerWidth - edgeMargin - width));
|
|
5968
|
+
const fitsBelow = pos.bottom + 8 + height <= window.innerHeight - edgeMargin;
|
|
5969
|
+
const fitsAbove = pos.top - 8 - height >= edgeMargin;
|
|
5970
|
+
if (placement === "bottom" && !fitsBelow && fitsAbove) placement = "top";
|
|
5971
|
+
else if (placement === "top" && !fitsAbove && fitsBelow) placement = "bottom";
|
|
5972
|
+
el.style.left = `${left + offset}px`;
|
|
5973
|
+
el.style.top = `${placement === "right" ? (pos.top + pos.bottom) / 2 : placement === "top" ? pos.top - 8 : pos.bottom + 8}px`;
|
|
5974
|
+
el.dataset.side = placement;
|
|
5975
|
+
el.style.visibility = "visible";
|
|
5976
|
+
};
|
|
5977
|
+
const observer = new ResizeObserver((entries) => {
|
|
5978
|
+
size = entries[0]?.borderBoxSize[0];
|
|
5979
|
+
fit();
|
|
5980
|
+
});
|
|
5981
|
+
observer.observe(el, { box: "border-box" });
|
|
5982
|
+
window.addEventListener("resize", fit);
|
|
5983
|
+
return () => {
|
|
5984
|
+
observer.disconnect();
|
|
5985
|
+
window.removeEventListener("resize", fit);
|
|
5986
|
+
};
|
|
5987
|
+
}, [
|
|
5988
|
+
align,
|
|
5989
|
+
pos,
|
|
5990
|
+
side,
|
|
5991
|
+
suppressed,
|
|
5992
|
+
visible
|
|
5993
|
+
]);
|
|
5980
5994
|
useEffect(() => {
|
|
5981
5995
|
announce(visible);
|
|
5982
5996
|
return () => {
|
|
@@ -6005,7 +6019,6 @@ function Tooltip({ label, side = "right", align = "center", delayMs = 0, disable
|
|
|
6005
6019
|
/* v8 ignore next -- the ref is attached by event time: events fire on the cloned anchor. */
|
|
6006
6020
|
if (el === null) return;
|
|
6007
6021
|
const r = el.getBoundingClientRect();
|
|
6008
|
-
setPlacement(side);
|
|
6009
6022
|
setPos({
|
|
6010
6023
|
x: side === "right" ? r.right + 10 : align === "end" ? r.right : r.left + r.width / 2,
|
|
6011
6024
|
top: r.top,
|
|
@@ -6035,12 +6048,13 @@ function Tooltip({ label, side = "right", align = "center", delayMs = 0, disable
|
|
|
6035
6048
|
const content = visible && !suppressed && jsx("span", {
|
|
6036
6049
|
ref: bubble,
|
|
6037
6050
|
className: css$20.bubble,
|
|
6038
|
-
"data-side":
|
|
6051
|
+
"data-side": side,
|
|
6039
6052
|
"data-portal": portal || void 0,
|
|
6040
6053
|
"data-align": align,
|
|
6041
6054
|
style: {
|
|
6042
6055
|
left: pos.x,
|
|
6043
6056
|
top: y,
|
|
6057
|
+
visibility: "hidden",
|
|
6044
6058
|
...maxWidth === void 0 ? {} : { maxWidth }
|
|
6045
6059
|
},
|
|
6046
6060
|
role: "tooltip",
|
|
@@ -8719,6 +8733,66 @@ function FoldToggle({ className, expanded, hidden, labels, onToggle }) {
|
|
|
8719
8733
|
});
|
|
8720
8734
|
}
|
|
8721
8735
|
//#endregion
|
|
8736
|
+
//#region lib/types/CodeToolbar.js
|
|
8737
|
+
/** Shared language, wrapping, and clipboard controls for code cards. */
|
|
8738
|
+
/**
|
|
8739
|
+
* Render a language label and keyboard-accessible icon actions with tooltips.
|
|
8740
|
+
* @param props - Localized labels, current state, and card-owned actions.
|
|
8741
|
+
* @returns The shared code-card header.
|
|
8742
|
+
*/
|
|
8743
|
+
function CodeToolbar({ lang, title, status, labels, copyLabel, copiedLabel, copied, wrapped, onCopy, onWrap }) {
|
|
8744
|
+
const wrapLabel = wrapped ? labels.unwrapLabel : labels.wrapLabel;
|
|
8745
|
+
const clipboardLabel = copied ? copiedLabel : copyLabel;
|
|
8746
|
+
return jsxs("div", {
|
|
8747
|
+
className: cardCss.header,
|
|
8748
|
+
"data-code-block-banner": true,
|
|
8749
|
+
children: [jsxs("div", {
|
|
8750
|
+
className: cardCss.heading,
|
|
8751
|
+
children: [jsx("span", {
|
|
8752
|
+
className: cardCss.language,
|
|
8753
|
+
children: supportsHighlighting(lang) ? lang : labels.codeLabel
|
|
8754
|
+
}), title !== void 0 && jsx("span", {
|
|
8755
|
+
className: cardCss.title,
|
|
8756
|
+
title,
|
|
8757
|
+
children: title
|
|
8758
|
+
})]
|
|
8759
|
+
}), jsxs("div", {
|
|
8760
|
+
className: cardCss.actions,
|
|
8761
|
+
children: [
|
|
8762
|
+
status !== void 0 && jsx("span", {
|
|
8763
|
+
className: cardCss.status,
|
|
8764
|
+
children: status
|
|
8765
|
+
}),
|
|
8766
|
+
onWrap !== void 0 && jsx(Tooltip, {
|
|
8767
|
+
label: wrapLabel,
|
|
8768
|
+
side: "top",
|
|
8769
|
+
portal: true,
|
|
8770
|
+
children: jsx("button", {
|
|
8771
|
+
type: "button",
|
|
8772
|
+
className: cardCss.action,
|
|
8773
|
+
"aria-label": labels.wrapLabel,
|
|
8774
|
+
"aria-pressed": wrapped,
|
|
8775
|
+
onClick: onWrap,
|
|
8776
|
+
children: wrapped ? jsx(IconNowrapFillRegular, { size: 14 }) : jsx(IconWrapFillRegular, { size: 14 })
|
|
8777
|
+
})
|
|
8778
|
+
}),
|
|
8779
|
+
onCopy !== void 0 && jsx(Tooltip, {
|
|
8780
|
+
label: clipboardLabel,
|
|
8781
|
+
side: "top",
|
|
8782
|
+
portal: true,
|
|
8783
|
+
children: jsx("button", {
|
|
8784
|
+
type: "button",
|
|
8785
|
+
className: cardCss.action,
|
|
8786
|
+
"aria-label": clipboardLabel,
|
|
8787
|
+
onClick: onCopy,
|
|
8788
|
+
children: copied ? jsx(IconCheckOutlineRegular, { size: 14 }) : jsx(IconCopyOutlineRegular, { size: 14 })
|
|
8789
|
+
})
|
|
8790
|
+
})
|
|
8791
|
+
]
|
|
8792
|
+
})]
|
|
8793
|
+
});
|
|
8794
|
+
}
|
|
8795
|
+
//#endregion
|
|
8722
8796
|
//#region lib/types/markdown/useViewportHighlighting.js
|
|
8723
8797
|
const noop = () => {};
|
|
8724
8798
|
/** One document-wide observer; activated elements leave it permanently. */
|
|
@@ -8817,6 +8891,7 @@ function ReadBlock({ label, labels, lines, totalLines, lang, maxLines = 16, clas
|
|
|
8817
8891
|
]);
|
|
8818
8892
|
const [expanded, setExpanded] = useState(false);
|
|
8819
8893
|
const [copied, setCopied] = useState(false);
|
|
8894
|
+
const [wrapped, setWrapped] = useState(false);
|
|
8820
8895
|
const onCopy = useCallback(() => {
|
|
8821
8896
|
if (copied) return;
|
|
8822
8897
|
writeClipboard(raw).then((ok) => {
|
|
@@ -8846,37 +8921,29 @@ function ReadBlock({ label, labels, lines, totalLines, lang, maxLines = 16, clas
|
|
|
8846
8921
|
children: spans === void 0 ? line.text : renderSpans(spans)
|
|
8847
8922
|
})]
|
|
8848
8923
|
}, line.number));
|
|
8924
|
+
const gutterStyle = { "--dsl-read-gutter": `${lines.reduce((digits, line) => Math.max(digits, String(line.number).length), 3)}ch` };
|
|
8849
8925
|
const paired = lines.map((line, index) => [line, highlighted?.[index]]);
|
|
8850
8926
|
return jsxs("div", {
|
|
8851
8927
|
ref: rootRef,
|
|
8852
|
-
className: clsx(css$26.block, className),
|
|
8928
|
+
className: clsx(cardCss.card, css$26.block, className),
|
|
8853
8929
|
"data-read": "",
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
}),
|
|
8870
|
-
lines.length > 0 && jsx("button", {
|
|
8871
|
-
type: "button",
|
|
8872
|
-
className: css$26.copyButton,
|
|
8873
|
-
onClick: onCopy,
|
|
8874
|
-
children: copied ? labels.copied : labels.copy
|
|
8875
|
-
})
|
|
8876
|
-
]
|
|
8877
|
-
})]
|
|
8930
|
+
"data-code-wrap": wrapped,
|
|
8931
|
+
style: gutterStyle,
|
|
8932
|
+
children: [jsx(CodeToolbar, {
|
|
8933
|
+
lang,
|
|
8934
|
+
title: label,
|
|
8935
|
+
status: windowed ? labels.window(lines.length, totalLines) : void 0,
|
|
8936
|
+
labels,
|
|
8937
|
+
copyLabel: labels.copy,
|
|
8938
|
+
copiedLabel: labels.copied,
|
|
8939
|
+
copied,
|
|
8940
|
+
wrapped,
|
|
8941
|
+
onCopy: lines.length > 0 ? onCopy : void 0,
|
|
8942
|
+
onWrap: () => {
|
|
8943
|
+
setWrapped((value) => !value);
|
|
8944
|
+
}
|
|
8878
8945
|
}), jsxs("div", {
|
|
8879
|
-
className:
|
|
8946
|
+
className: cardCss.body,
|
|
8880
8947
|
children: [
|
|
8881
8948
|
rows(capped ? paired.slice(0, headLines) : paired),
|
|
8882
8949
|
hidden > 0 && jsx(FoldToggle, {
|
|
@@ -8925,7 +8992,7 @@ function localHunks(diff) {
|
|
|
8925
8992
|
* comparisons exceeding the edit limit count both complete fragments as replaced.
|
|
8926
8993
|
* Text follows {@link contentLines}'s terminator rule.
|
|
8927
8994
|
* @param diffs - the hunks to count.
|
|
8928
|
-
* @returns the +/- totals for summaries
|
|
8995
|
+
* @returns the +/- totals for tool summaries.
|
|
8929
8996
|
*/
|
|
8930
8997
|
function diffTotals(diffs) {
|
|
8931
8998
|
let added = 0;
|
|
@@ -8940,18 +9007,16 @@ function diffTotals(diffs) {
|
|
|
8940
9007
|
};
|
|
8941
9008
|
}
|
|
8942
9009
|
/**
|
|
8943
|
-
* Flatten local patches into rows
|
|
9010
|
+
* Flatten local patches into rows.
|
|
8944
9011
|
* A path header opens each new file. A `⋯` gap separates consecutive same-file
|
|
8945
|
-
* fragments and distant patches within a fragment.
|
|
9012
|
+
* fragments and distant patches within a fragment.
|
|
8946
9013
|
* @param diffs - the hunks to render.
|
|
8947
|
-
* @returns the body rows
|
|
9014
|
+
* @returns the body rows.
|
|
8948
9015
|
*/
|
|
8949
9016
|
function buildRows(diffs) {
|
|
8950
9017
|
const rows = [];
|
|
8951
|
-
const paths = /* @__PURE__ */ new Set();
|
|
8952
9018
|
let prevPath;
|
|
8953
9019
|
for (const diff of diffs) {
|
|
8954
|
-
paths.add(diff.path);
|
|
8955
9020
|
if (diff.path !== prevPath) rows.push({
|
|
8956
9021
|
kind: "path",
|
|
8957
9022
|
text: diff.path
|
|
@@ -8975,12 +9040,7 @@ function buildRows(diffs) {
|
|
|
8975
9040
|
}
|
|
8976
9041
|
}
|
|
8977
9042
|
}
|
|
8978
|
-
return
|
|
8979
|
-
rows,
|
|
8980
|
-
added: rows.filter((row) => row.kind === "add").length,
|
|
8981
|
-
removed: rows.filter((row) => row.kind === "del").length,
|
|
8982
|
-
files: paths.size
|
|
8983
|
-
};
|
|
9043
|
+
return rows;
|
|
8984
9044
|
}
|
|
8985
9045
|
/**
|
|
8986
9046
|
* Split a side's text into its content lines. Empty text is zero lines (a full
|
|
@@ -9020,9 +9080,12 @@ function copyText$1(rows) {
|
|
|
9020
9080
|
* @returns the diff block element.
|
|
9021
9081
|
*/
|
|
9022
9082
|
function DiffBlock({ diffs, labels, maxLines = 16, className }) {
|
|
9023
|
-
const
|
|
9083
|
+
const rows = useMemo(() => buildRows(diffs), [diffs]);
|
|
9024
9084
|
const [expanded, setExpanded] = useState(false);
|
|
9025
9085
|
const [copied, setCopied] = useState(false);
|
|
9086
|
+
const [wrapped, setWrapped] = useState(false);
|
|
9087
|
+
const firstLanguage = diffs[0] === void 0 ? void 0 : languageForPath(diffs[0].path);
|
|
9088
|
+
const language = diffs.every((diff) => languageForPath(diff.path) === firstLanguage) ? firstLanguage : void 0;
|
|
9026
9089
|
const onCopy = useCallback(() => {
|
|
9027
9090
|
if (copied) return;
|
|
9028
9091
|
writeClipboard(copyText$1(rows)).then((ok) => {
|
|
@@ -9044,47 +9107,40 @@ function DiffBlock({ diffs, labels, maxLines = 16, className }) {
|
|
|
9044
9107
|
const head = capped ? rows.slice(0, headLines) : rows;
|
|
9045
9108
|
const tail = capped ? rows.slice(rows.length - tailLines) : [];
|
|
9046
9109
|
return jsxs("div", {
|
|
9047
|
-
className: clsx(css$27.block, className),
|
|
9110
|
+
className: clsx(cardCss.card, css$27.block, className),
|
|
9048
9111
|
"data-diff": "",
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
" -",
|
|
9082
|
-
removed,
|
|
9083
|
-
" · ",
|
|
9084
|
-
labels.files(files)
|
|
9085
|
-
]
|
|
9086
|
-
})
|
|
9087
|
-
]
|
|
9112
|
+
"data-code-wrap": wrapped,
|
|
9113
|
+
children: [jsx(CodeToolbar, {
|
|
9114
|
+
lang: language,
|
|
9115
|
+
labels,
|
|
9116
|
+
copyLabel: labels.copy,
|
|
9117
|
+
copiedLabel: labels.copied,
|
|
9118
|
+
copied,
|
|
9119
|
+
wrapped,
|
|
9120
|
+
onCopy,
|
|
9121
|
+
onWrap: () => {
|
|
9122
|
+
setWrapped((value) => !value);
|
|
9123
|
+
}
|
|
9124
|
+
}), jsxs("div", {
|
|
9125
|
+
className: css$27.body,
|
|
9126
|
+
children: [
|
|
9127
|
+
head.map((row, index) => jsx("div", {
|
|
9128
|
+
className: clsx(css$27.line, ROW_CLASS[row.kind]),
|
|
9129
|
+
children: row.text
|
|
9130
|
+
}, index)),
|
|
9131
|
+
hidden > 0 && jsx(FoldToggle, {
|
|
9132
|
+
className: css$27.expand,
|
|
9133
|
+
expanded,
|
|
9134
|
+
hidden,
|
|
9135
|
+
labels,
|
|
9136
|
+
onToggle
|
|
9137
|
+
}),
|
|
9138
|
+
tail.map((row, index) => jsx("div", {
|
|
9139
|
+
className: clsx(css$27.line, ROW_CLASS[row.kind]),
|
|
9140
|
+
children: row.text
|
|
9141
|
+
}, index))
|
|
9142
|
+
]
|
|
9143
|
+
})]
|
|
9088
9144
|
});
|
|
9089
9145
|
}
|
|
9090
9146
|
//#endregion
|
|
@@ -9965,7 +10021,7 @@ function renderLine(line, index) {
|
|
|
9965
10021
|
}, spanIndex))
|
|
9966
10022
|
})] }, index);
|
|
9967
10023
|
}
|
|
9968
|
-
function CodeBlock({ code, lang, streaming, className, contentRef, lineNumbers = false, showHeader = true, copyLabel, copiedLabel }) {
|
|
10024
|
+
function CodeBlock({ code, lang, streaming, className, contentRef, lineNumbers = false, showHeader = true, copyLabel, copiedLabel, toolbarLabels, wrap }) {
|
|
9969
10025
|
const trimmed = code.endsWith("\n") ? code.slice(0, -1) : code;
|
|
9970
10026
|
const sourceLines = lineNumbers ? trimmed.split("\n") : void 0;
|
|
9971
10027
|
const rootRef = useRef(null);
|
|
@@ -10050,6 +10106,8 @@ function CodeBlock({ code, lang, streaming, className, contentRef, lineNumbers =
|
|
|
10050
10106
|
loaded
|
|
10051
10107
|
]);
|
|
10052
10108
|
const [copied, setCopied] = useState(false);
|
|
10109
|
+
const [localWrapped, setWrapped] = useState(true);
|
|
10110
|
+
const wrapped = wrap ?? localWrapped;
|
|
10053
10111
|
const onCopy = useCallback(() => {
|
|
10054
10112
|
if (copied) return;
|
|
10055
10113
|
writeClipboard(rootRef.current?.querySelector("pre")?.textContent ?? trimmed).then((ok) => {
|
|
@@ -10069,12 +10127,24 @@ function CodeBlock({ code, lang, streaming, className, contentRef, lineNumbers =
|
|
|
10069
10127
|
}) : jsx("div", { dangerouslySetInnerHTML: { __html: html } });
|
|
10070
10128
|
return jsxs("div", {
|
|
10071
10129
|
ref: rootRef,
|
|
10072
|
-
className: clsx(css$29.block, "md-code-block", lineNumbers && css$29.numbered, className),
|
|
10130
|
+
className: clsx(css$29.block, "md-code-block", lineNumbers && css$29.numbered, toolbarLabels !== void 0 && css$29.card, className),
|
|
10073
10131
|
"data-line-numbers": lineNumbers || void 0,
|
|
10132
|
+
"data-code-wrap": toolbarLabels === void 0 ? void 0 : wrapped,
|
|
10074
10133
|
style: sourceLines === void 0 ? void 0 : { "--dsl-code-block-line-number-width": `${Math.max(2, String(sourceLines.length).length)}ch` },
|
|
10075
10134
|
children: [showHeader && jsx("div", {
|
|
10076
10135
|
className: css$29.bannerWrap,
|
|
10077
|
-
children:
|
|
10136
|
+
children: toolbarLabels !== void 0 ? jsx(CodeToolbar, {
|
|
10137
|
+
lang,
|
|
10138
|
+
labels: toolbarLabels,
|
|
10139
|
+
copyLabel,
|
|
10140
|
+
copiedLabel,
|
|
10141
|
+
copied,
|
|
10142
|
+
wrapped,
|
|
10143
|
+
onCopy,
|
|
10144
|
+
onWrap: wrap === void 0 ? () => {
|
|
10145
|
+
setWrapped((value) => !value);
|
|
10146
|
+
} : void 0
|
|
10147
|
+
}) : jsxs("div", {
|
|
10078
10148
|
className: css$29.banner,
|
|
10079
10149
|
"data-code-block-banner": true,
|
|
10080
10150
|
children: [jsx("div", {
|
|
@@ -10434,7 +10504,8 @@ function renderCode(node, key, context) {
|
|
|
10434
10504
|
lang,
|
|
10435
10505
|
streaming: context.streaming,
|
|
10436
10506
|
copyLabel: context.labels.code.copyLabel,
|
|
10437
|
-
copiedLabel: context.labels.code.copiedLabel
|
|
10507
|
+
copiedLabel: context.labels.code.copiedLabel,
|
|
10508
|
+
toolbarLabels: context.labels.code.toolbarLabels
|
|
10438
10509
|
}, key);
|
|
10439
10510
|
}
|
|
10440
10511
|
/** A list is loose when it or any of its items is spread; every item then keeps its paragraphs. */
|
|
@@ -104,6 +104,21 @@
|
|
|
104
104
|
color: var(--dsw-alias-label-primary);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
.card :where(pre) {
|
|
108
|
+
padding: 6px 22px 20px;
|
|
109
|
+
word-break: normal;
|
|
110
|
+
overflow-wrap: anywhere;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.card[data-code-wrap='false'] {
|
|
114
|
+
--dsl-code-block-line-white-space: pre;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.card[data-code-wrap='false'] :where(pre) {
|
|
118
|
+
white-space: pre;
|
|
119
|
+
overflow-wrap: normal;
|
|
120
|
+
}
|
|
121
|
+
|
|
107
122
|
.numbered :where(pre) code {
|
|
108
123
|
display: block;
|
|
109
124
|
counter-reset: source-line;
|
|
@@ -401,24 +401,6 @@
|
|
|
401
401
|
z-index: auto;
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
-
.compact [data-code-block-banner] {
|
|
405
|
-
padding: 4px 8px;
|
|
406
|
-
font: inherit;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
.compact [data-code-block-banner] > :first-child {
|
|
410
|
-
font-size: inherit;
|
|
411
|
-
line-height: inherit;
|
|
412
|
-
color: inherit;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
.compact pre {
|
|
416
|
-
margin: 4px 0;
|
|
417
|
-
padding: 8px;
|
|
418
|
-
font-size: inherit;
|
|
419
|
-
line-height: inherit;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
404
|
/* Shiki writes token colors inline; compact code uses the surrounding text color. */
|
|
423
405
|
.compact pre,
|
|
424
406
|
.compact pre :is(code, span) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Localized language fallback and wrapping actions supplied by the card owner. */
|
|
2
|
+
export interface CodeToolbarLabels {
|
|
3
|
+
/** Title for an absent or unsupported language. */
|
|
4
|
+
codeLabel: string;
|
|
5
|
+
/** Action that enables wrapping. */
|
|
6
|
+
wrapLabel: string;
|
|
7
|
+
/** Action that preserves source columns with horizontal scrolling. */
|
|
8
|
+
unwrapLabel: string;
|
|
9
|
+
}
|
|
10
|
+
/** Display state and callbacks for a code card's toolbar. */
|
|
11
|
+
interface CodeToolbarProps {
|
|
12
|
+
lang?: string | undefined;
|
|
13
|
+
title?: string | undefined;
|
|
14
|
+
status?: string | undefined;
|
|
15
|
+
labels: CodeToolbarLabels;
|
|
16
|
+
copyLabel: string;
|
|
17
|
+
copiedLabel: string;
|
|
18
|
+
copied: boolean;
|
|
19
|
+
wrapped: boolean;
|
|
20
|
+
onCopy?: (() => void) | undefined;
|
|
21
|
+
onWrap?: (() => void) | undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Render a language label and keyboard-accessible icon actions with tooltips.
|
|
25
|
+
* @param props - Localized labels, current state, and card-owned actions.
|
|
26
|
+
* @returns The shared code-card header.
|
|
27
|
+
*/
|
|
28
|
+
export declare function CodeToolbar({ lang, title, status, labels, copyLabel, copiedLabel, copied, wrapped, onCopy, onWrap }: CodeToolbarProps): import("react").JSX.Element;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=CodeToolbar.d.ts.map
|
package/lib/types/DiffBlock.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CodeToolbarLabels } from './CodeToolbar.tsx';
|
|
1
2
|
/** Output lines shown before the height cap collapses the middle. */
|
|
2
3
|
export declare const DEFAULT_DIFF_MAX_LINES = 16;
|
|
3
4
|
/**
|
|
@@ -23,21 +24,20 @@ export interface DiffBlockProps {
|
|
|
23
24
|
className?: string | undefined;
|
|
24
25
|
}
|
|
25
26
|
/** Localized chrome for {@link DiffBlock}. */
|
|
26
|
-
export interface DiffBlockLabels {
|
|
27
|
+
export interface DiffBlockLabels extends CodeToolbarLabels {
|
|
27
28
|
copy: string;
|
|
28
29
|
copied: string;
|
|
29
30
|
collapseAria: string;
|
|
30
31
|
expandAria: (hidden: number) => string;
|
|
31
32
|
collapse: string;
|
|
32
33
|
expand: (hidden: number) => string;
|
|
33
|
-
files: (count: number) => string;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* Count displayed additions and deletions. Exact patches exclude shared context;
|
|
37
37
|
* comparisons exceeding the edit limit count both complete fragments as replaced.
|
|
38
38
|
* Text follows {@link contentLines}'s terminator rule.
|
|
39
39
|
* @param diffs - the hunks to count.
|
|
40
|
-
* @returns the +/- totals for summaries
|
|
40
|
+
* @returns the +/- totals for tool summaries.
|
|
41
41
|
*/
|
|
42
42
|
export declare function diffTotals(diffs: DiffHunk[]): {
|
|
43
43
|
added: number;
|
package/lib/types/Menu.d.ts
CHANGED
|
@@ -79,7 +79,7 @@ export declare function MenuItemButton({ children, icon, disabled, danger, separ
|
|
|
79
79
|
* @param props.align - list alignment against the anchor (default 'start').
|
|
80
80
|
* @param props.side - open below (`bottom`, default) or above (`top`) the anchor.
|
|
81
81
|
* @param props.portal - render the list into document.body, fixed-positioned
|
|
82
|
-
* from the anchor rect (
|
|
82
|
+
* from the anchor rect (follows movement and resizing while open). Use when an
|
|
83
83
|
* ancestor's overflow clipping would crop the in-place list; default false
|
|
84
84
|
* keeps the pure-CSS in-place behavior.
|
|
85
85
|
* @param props.closeOnPointerLeave - close the list once the pointer has left
|
|
@@ -92,8 +92,8 @@ export declare function MenuItemButton({ children, icon, disabled, danger, separ
|
|
|
92
92
|
* directly (e.g. from a host-owned trigger button) instead of measuring the
|
|
93
93
|
* Menu's own wrapper span. Required when the wrapper isn't itself laid out at
|
|
94
94
|
* the trigger (render-prop anchors, effect-positioned proxies — measuring the
|
|
95
|
-
* wrapper there races the host's layout effects). Called on open
|
|
96
|
-
* scroll/resize; return null to skip placement for that frame.
|
|
95
|
+
* wrapper there races the host's layout effects). Called on open, each animation
|
|
96
|
+
* frame, and scroll/resize; return null to skip placement for that frame.
|
|
97
97
|
* @param props.footer - rows pinned below the scrolling items area, separated
|
|
98
98
|
* by a hairline; they stay visible while the items above scroll.
|
|
99
99
|
* @param props.children - component rows rendered after `items` in the same
|
package/lib/types/ReadBlock.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CodeToolbarLabels } from './CodeToolbar.tsx';
|
|
1
2
|
/**
|
|
2
3
|
* Content lines shown before the height cap collapses the middle. Matches
|
|
3
4
|
* TerminalBlock's default so a long read and a long command output cut at the
|
|
@@ -28,7 +29,7 @@ export interface ReadBlockProps {
|
|
|
28
29
|
className?: string | undefined;
|
|
29
30
|
}
|
|
30
31
|
/** Localized chrome for {@link ReadBlock}. */
|
|
31
|
-
export interface ReadBlockLabels {
|
|
32
|
+
export interface ReadBlockLabels extends CodeToolbarLabels {
|
|
32
33
|
window: (shown: number, total: number) => string;
|
|
33
34
|
copy: string;
|
|
34
35
|
copied: string;
|
package/lib/types/Tooltip.d.ts
CHANGED
|
@@ -26,7 +26,8 @@ type TooltipLabel = string | (() => string);
|
|
|
26
26
|
* @param props.maxWidth - bubble width cap in pixels, for labels long enough that the default
|
|
27
27
|
* half-viewport cap would render a slab wider than the surface the anchor sits on.
|
|
28
28
|
* @param props.children - a single anchor element; its own ref (callback or object) is forwarded alongside the tooltip's.
|
|
29
|
-
* @returns the cloned anchor plus a fixed-position bubble, optionally portaled to the body
|
|
29
|
+
* @returns the cloned anchor plus a fixed-position bubble, optionally portaled to the body.
|
|
30
|
+
* The bubble stays hidden until ResizeObserver supplies its size for viewport fitting; clicking the
|
|
30
31
|
* anchor dismisses the bubble until the next trigger, and focus arriving after a pointer
|
|
31
32
|
* interaction (a closing menu refocusing its trigger) never raises it.
|
|
32
33
|
*/
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cordis-free React primitives styled only through `--dsw-*` tokens.
|
|
3
3
|
*/
|
|
4
|
+
export type { CodeToolbarLabels } from './CodeToolbar.tsx';
|
|
4
5
|
export { StateDot } from './StateDot.tsx';
|
|
5
6
|
export type { StateDotState } from './StateDot.tsx';
|
|
6
7
|
export { DisclosureRow } from './DisclosureRow.tsx';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Ref } from 'react';
|
|
2
|
+
import { type CodeToolbarLabels } from '../CodeToolbar.tsx';
|
|
2
3
|
export interface CodeBlockProps {
|
|
3
4
|
/** The source text, rendered verbatim (trailing newline trimmed for display). */
|
|
4
5
|
code: string;
|
|
@@ -25,6 +26,10 @@ export interface CodeBlockProps {
|
|
|
25
26
|
copyLabel: string;
|
|
26
27
|
/** Copy-button label during the post-copy confirmation window. */
|
|
27
28
|
copiedLabel: string;
|
|
29
|
+
/** Enable the shared card toolbar and spacing; omit for custom toolbar layouts. */
|
|
30
|
+
toolbarLabels?: CodeToolbarLabels | undefined;
|
|
31
|
+
/** With toolbarLabels, use the owner's wrapping preference and omit the toolbar's local wrap action. */
|
|
32
|
+
wrap?: boolean | undefined;
|
|
28
33
|
}
|
|
29
|
-
export declare function CodeBlock({ code, lang, streaming, className, contentRef, lineNumbers, showHeader, copyLabel, copiedLabel, }: CodeBlockProps): import("react").JSX.Element;
|
|
34
|
+
export declare function CodeBlock({ code, lang, streaming, className, contentRef, lineNumbers, showHeader, copyLabel, copiedLabel, toolbarLabels, wrap, }: CodeBlockProps): import("react").JSX.Element;
|
|
30
35
|
//# sourceMappingURL=CodeBlock.d.ts.map
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { ReactNode } from 'react';
|
|
19
19
|
import type * as Md from 'mdast';
|
|
20
|
+
import type { CodeToolbarLabels } from '../CodeToolbar.tsx';
|
|
20
21
|
import type { PositionedBlock } from './incremental.ts';
|
|
21
22
|
/** Copy-button labels forwarded to fence CodeBlocks (this package is cordis-free, so copy arrives via props). */
|
|
22
23
|
export interface MarkdownCodeLabels {
|
|
@@ -24,6 +25,8 @@ export interface MarkdownCodeLabels {
|
|
|
24
25
|
copyLabel: string;
|
|
25
26
|
/** Copy-button label during the post-copy confirmation window. */
|
|
26
27
|
copiedLabel: string;
|
|
28
|
+
/** Shared card controls; omitted for custom toolbar layouts. */
|
|
29
|
+
toolbarLabels?: CodeToolbarLabels | undefined;
|
|
27
30
|
}
|
|
28
31
|
/** Localized chrome for a Markdown document. */
|
|
29
32
|
export interface MarkdownLabels {
|
package/lib/types/user-text.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Display projection of reference forms in sent user text (bubble and queue
|
|
3
3
|
* rows). The logged model text remains the single truth; this is presentation
|
|
4
|
-
* only
|
|
5
|
-
*
|
|
4
|
+
* only. Inline references follow the consumer's wrapping policy and keep long
|
|
5
|
+
* labels within its width. Four decoration sources, by precedence: the wire session form
|
|
6
6
|
* `@[label](dsh-session:...)` folds to its label; exact session labels
|
|
7
7
|
* supplied by an adjacent recall decorate their bare `@label` mention; plain
|
|
8
8
|
* `@name` word-boundary tokens decorate by shape alone; and a plain `/name`
|
package/lib/user-text.module.css
CHANGED
|
@@ -14,10 +14,13 @@
|
|
|
14
14
|
chip's label rode ~3px above the surrounding text in both surfaces. An
|
|
15
15
|
inline chip shares the consumer's baseline by construction. */
|
|
16
16
|
display: inline;
|
|
17
|
+
max-width: 100%;
|
|
17
18
|
margin: 0 2px;
|
|
18
19
|
color: var(--dsw-alias-state-business-primary);
|
|
19
20
|
font-weight: 500;
|
|
20
|
-
white-space:
|
|
21
|
+
white-space: inherit;
|
|
22
|
+
overflow-wrap: anywhere;
|
|
23
|
+
text-align: inherit;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
/* Skill and command tokens read as code: the theme's code family at the
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-client-ui-primitives",
|
|
3
3
|
"description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)",
|
|
4
|
-
"version": "0.1.7-alpha.
|
|
4
|
+
"version": "0.1.7-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"micromark-extension-gfm": "^3.0.0",
|
|
48
48
|
"micromark-util-sanitize-uri": "^2.0.1",
|
|
49
49
|
"simple-icons": "16.31.0",
|
|
50
|
-
"@deepseek-ai/
|
|
51
|
-
"@deepseek-ai/
|
|
52
|
-
"@deepseek-ai/dsh-
|
|
50
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
51
|
+
"@deepseek-ai/dsh-util-workspace-path": "0.1.7-alpha.2",
|
|
52
|
+
"@deepseek-ai/dsh-client-store": "0.1.7-alpha.2"
|
|
53
53
|
},
|
|
54
54
|
"files": [
|
|
55
55
|
"lib/index.js",
|
|
@@ -57,6 +57,6 @@
|
|
|
57
57
|
"lib/types/**/*.d.ts"
|
|
58
58
|
],
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@deepseek-ai/cordis": "
|
|
60
|
+
"@deepseek-ai/cordis": "~4.0.4"
|
|
61
61
|
}
|
|
62
62
|
}
|